diff options
189 files changed, 38267 insertions, 0 deletions
diff --git a/Auth/OpenID.php b/Auth/OpenID.php new file mode 100644 index 0000000..6556b5b --- /dev/null +++ b/Auth/OpenID.php @@ -0,0 +1,552 @@ +<?php + +/** + * This is the PHP OpenID library by JanRain, Inc. + * + * This module contains core utility functionality used by the + * library. See Consumer.php and Server.php for the consumer and + * server implementations. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @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 + * an error page, since we do not have enough information to redirect + * back to the consumer. The associated value is an error message that + * should be displayed on an HTML error page. + * + * @see Auth_OpenID_Server + */ +define('Auth_OpenID_LOCAL_ERROR', 'local_error'); + +/** + * Status code returned when there is an error to return in key-value + * form to the consumer. The caller should return a 400 Bad Request + * response with content-type text/plain and the value as the body. + * + * @see Auth_OpenID_Server + */ +define('Auth_OpenID_REMOTE_ERROR', 'remote_error'); + +/** + * Status code returned when there is a key-value form OK response to + * the consumer. The value associated with this code is the + * response. The caller should return a 200 OK response with + * content-type text/plain and the value as the body. + * + * @see Auth_OpenID_Server + */ +define('Auth_OpenID_REMOTE_OK', 'remote_ok'); + +/** + * Status code returned when there is a redirect back to the + * consumer. The value is the URL to redirect back to. The caller + * should return a 302 Found redirect with a Location: header + * containing the URL. + * + * @see Auth_OpenID_Server + */ +define('Auth_OpenID_REDIRECT', 'redirect'); + +/** + * Status code returned when the caller needs to authenticate the + * user. The associated value is a {@link Auth_OpenID_ServerRequest} + * object that can be used to complete the authentication. If the user + * has taken some authentication action, use the retry() method of the + * {@link Auth_OpenID_ServerRequest} object to complete the request. + * + * @see Auth_OpenID_Server + */ +define('Auth_OpenID_DO_AUTH', 'do_auth'); + +/** + * Status code returned when there were no OpenID arguments + * passed. This code indicates that the caller should return a 200 OK + * response and display an HTML page that says that this is an OpenID + * server endpoint. + * + * @see Auth_OpenID_Server + */ +define('Auth_OpenID_DO_ABOUT', 'do_about'); + +/** + * Defines for regexes and format checking. + */ +define('Auth_OpenID_letters', + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"); + +define('Auth_OpenID_digits', + "0123456789"); + +define('Auth_OpenID_punct', + "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"); + +if (Auth_OpenID_getMathLib() === null) { + Auth_OpenID_setNoMathSupport(); +} + +/** + * The OpenID utility function class. + * + * @package OpenID + * @access private + */ +class Auth_OpenID { + + /** + * Return true if $thing is an Auth_OpenID_FailureResponse object; + * false if not. + * + * @access private + */ + function isFailure($thing) + { + return is_a($thing, 'Auth_OpenID_FailureResponse'); + } + + /** + * Gets the query data from the server environment based on the + * request method used. If GET was used, this looks at + * $_SERVER['QUERY_STRING'] directly. If POST was used, this + * fetches data from the special php://input file stream. + * + * Returns an associative array of the query arguments. + * + * Skips invalid key/value pairs (i.e. keys with no '=value' + * portion). + * + * Returns an empty array if neither GET nor POST was used, or if + * POST was used but php://input cannot be opened. + * + * @access private + */ + function getQuery($query_str=null) + { + $data = array(); + + if ($query_str !== null) { + $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) { + $post = array(); + } else { + $post = Auth_OpenID::params_from_string($str); + } + + $data = array_merge($data, $post); + } + } + + return $data; + } + + function params_from_string($str) + { + $chunks = explode("&", $str); + + $data = array(); + foreach ($chunks as $chunk) { + $parts = explode("=", $chunk, 2); + + if (count($parts) != 2) { + continue; + } + + list($k, $v) = $parts; + $data[$k] = urldecode($v); + } + + return $data; + } + + /** + * Create dir_name as a directory if it does not exist. If it + * exists, make sure that it is, in fact, a directory. Returns + * true if the operation succeeded; false if not. + * + * @access private + */ + function ensureDir($dir_name) + { + if (is_dir($dir_name) || @mkdir($dir_name)) { + return true; + } else { + $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)); + } + } + + /** + * Adds a string prefix to all values of an array. Returns a new + * array containing the prefixed values. + * + * @access private + */ + function addPrefix($values, $prefix) + { + $new_values = array(); + foreach ($values as $s) { + $new_values[] = $prefix . $s; + } + return $new_values; + } + + /** + * Convenience function for getting array values. Given an array + * $arr and a key $key, get the corresponding value from the array + * or return $default if the key is absent. + * + * @access private + */ + function arrayGet($arr, $key, $fallback = null) + { + if (is_array($arr)) { + if (array_key_exists($key, $arr)) { + return $arr[$key]; + } else { + return $fallback; + } + } else { + trigger_error("Auth_OpenID::arrayGet (key = ".$key.") expected " . + "array as first parameter, got " . + gettype($arr), E_USER_WARNING); + + return false; + } + } + + /** + * Replacement for PHP's broken parse_str. + */ + function parse_str($query) + { + if ($query === null) { + return null; + } + + $parts = explode('&', $query); + + $new_parts = array(); + for ($i = 0; $i < count($parts); $i++) { + $pair = explode('=', $parts[$i]); + + if (count($pair) != 2) { + continue; + } + + list($key, $value) = $pair; + $new_parts[$key] = urldecode($value); + } + + return $new_parts; + } + + /** + * Implements the PHP 5 'http_build_query' functionality. + * + * @access private + * @param array $data Either an array key/value pairs or an array + * of arrays, each of which holding two values: a key and a value, + * sequentially. + * @return string $result The result of url-encoding the key/value + * pairs from $data into a URL query string + * (e.g. "username=bob&id=56"). + */ + function httpBuildQuery($data) + { + $pairs = array(); + foreach ($data as $key => $value) { + if (is_array($value)) { + $pairs[] = urlencode($value[0])."=".urlencode($value[1]); + } else { + $pairs[] = urlencode($key)."=".urlencode($value); + } + } + return implode("&", $pairs); + } + + /** + * "Appends" query arguments onto a URL. The URL may or may not + * already have arguments (following a question mark). + * + * @access private + * @param string $url A URL, which may or may not already have + * arguments. + * @param array $args Either an array key/value pairs or an array of + * arrays, each of which holding two values: a key and a value, + * sequentially. If $args is an ordinary key/value array, the + * parameters will be added to the URL in sorted alphabetical order; + * if $args is an array of arrays, their order will be preserved. + * @return string $url The original URL with the new parameters added. + * + */ + function appendArgs($url, $args) + { + if (count($args) == 0) { + return $url; + } + + // Non-empty array; if it is an array of arrays, use + // multisort; otherwise use sort. + if (array_key_exists(0, $args) && + is_array($args[0])) { + // Do nothing here. + } else { + $keys = array_keys($args); + sort($keys); + $new_args = array(); + foreach ($keys as $key) { + $new_args[] = array($key, $args[$key]); + } + $args = $new_args; + } + + $sep = '?'; + if (strpos($url, '?') !== false) { + $sep = '&'; + } + + return $url . $sep . Auth_OpenID::httpBuildQuery($args); + } + + /** + * Implements python's urlunparse, which is not available in PHP. + * Given the specified components of a URL, this function rebuilds + * and returns the URL. + * + * @access private + * @param string $scheme The scheme (e.g. 'http'). Defaults to 'http'. + * @param string $host The host. Required. + * @param string $port The port. + * @param string $path The path. + * @param string $query The query. + * @param string $fragment The fragment. + * @return string $url The URL resulting from assembling the + * specified components. + */ + function urlunparse($scheme, $host, $port = null, $path = '/', + $query = '', $fragment = '') + { + + if (!$scheme) { + $scheme = 'http'; + } + + if (!$host) { + return false; + } + + if (!$path) { + $path = ''; + } + + $result = $scheme . "://" . $host; + + if ($port) { + $result .= ":" . $port; + } + + $result .= $path; + + if ($query) { + $result .= "?" . $query; + } + + if ($fragment) { + $result .= "#" . $fragment; + } + + return $result; + } + + /** + * Given a URL, this "normalizes" it by adding a trailing slash + * and / or a leading http:// scheme where necessary. Returns + * null if the original URL is malformed and cannot be normalized. + * + * @access private + * @param string $url The URL to be normalized. + * @return mixed $new_url The URL after normalization, or null if + * $url was malformed. + */ + function normalizeUrl($url) + { + @$parsed = parse_url($url); + + if (!$parsed) { + return null; + } + + if (isset($parsed['scheme']) && + isset($parsed['host'])) { + $scheme = strtolower($parsed['scheme']); + if (!in_array($scheme, array('http', 'https'))) { + return null; + } + } else { + $url = 'http://' . $url; + } + + $normalized = Auth_OpenID_urinorm($url); + if ($normalized === null) { + return null; + } + list($defragged, $frag) = Auth_OpenID::urldefrag($normalized); + return $defragged; + } + + /** + * Replacement (wrapper) for PHP's intval() because it's broken. + * + * @access private + */ + function intval($value) + { + $re = "/^\\d+$/"; + + if (!preg_match($re, $value)) { + return false; + } + + return intval($value); + } + + /** + * Count the number of bytes in a string independently of + * multibyte support conditions. + * + * @param string $str The string of bytes to count. + * @return int The number of bytes in $str. + */ + function bytes($str) + { + return strlen(bin2hex($str)) / 2; + } + + /** + * Get the bytes in a string independently of multibyte support + * conditions. + */ + function toBytes($str) + { + $hex = bin2hex($str); + + if (!$hex) { + return array(); + } + + $b = array(); + for ($i = 0; $i < strlen($hex); $i += 2) { + $b[] = chr(base_convert(substr($hex, $i, 2), 16, 10)); + } + + return $b; + } + + 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/Auth/OpenID/AX.php b/Auth/OpenID/AX.php new file mode 100644 index 0000000..4a617ae --- /dev/null +++ b/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/Auth/OpenID/Association.php b/Auth/OpenID/Association.php new file mode 100644 index 0000000..37ce0cb --- /dev/null +++ b/Auth/OpenID/Association.php @@ -0,0 +1,613 @@ +<?php + +/** + * This module contains code for dealing with associations between + * consumers and servers. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * @access private + */ +require_once 'Auth/OpenID/CryptUtil.php'; + +/** + * @access private + */ +require_once 'Auth/OpenID/KVForm.php'; + +/** + * @access private + */ +require_once 'Auth/OpenID/HMAC.php'; + +/** + * This class represents an association between a server and a + * consumer. In general, users of this library will never see + * instances of this object. The only exception is if you implement a + * custom {@link Auth_OpenID_OpenIDStore}. + * + * If you do implement such a store, it will need to store the values + * of the handle, secret, issued, lifetime, and assoc_type instance + * variables. + * + * @package OpenID + */ +class Auth_OpenID_Association { + + /** + * This is a HMAC-SHA1 specific value. + * + * @access private + */ + var $SIG_LENGTH = 20; + + /** + * The ordering and name of keys as stored by serialize. + * + * @access private + */ + var $assoc_keys = array( + 'version', + 'handle', + 'secret', + 'issued', + 'lifetime', + '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 + * implementations shouldn't use this constructor. + * + * @access private + * + * @param integer $expires_in This is the amount of time this + * association is good for, measured in seconds since the + * association was issued. + * + * @param string $handle This is the handle the server gave this + * association. + * + * @param string secret This is the shared secret the server + * generated for this association. + * + * @param assoc_type This is the type of association this + * 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. + */ + function fromExpiresIn($expires_in, $handle, $secret, $assoc_type) + { + $issued = time(); + $lifetime = $expires_in; + return new Auth_OpenID_Association($handle, $secret, + $issued, $lifetime, $assoc_type); + } + + /** + * This is the standard constructor for creating an association. + * The library should create all of the necessary associations, so + * this constructor is not part of the external API. + * + * @access private + * + * @param string $handle This is the handle the server gave this + * association. + * + * @param string $secret This is the shared secret the server + * generated for this association. + * + * @param integer $issued This is the time this association was + * issued, in seconds since 00:00 GMT, January 1, 1970. (ie, a + * unix timestamp) + * + * @param integer $lifetime This is the amount of time this + * association is good for, measured in seconds since the + * association was issued. + * + * @param string $assoc_type This is the type of association this + * 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) + { + if (!in_array($assoc_type, + Auth_OpenID_getSupportedAssociationTypes())) { + $fmt = 'Unsupported association type (%s)'; + trigger_error(sprintf($fmt, $assoc_type), E_USER_ERROR); + } + + $this->handle = $handle; + $this->secret = $secret; + $this->issued = $issued; + $this->lifetime = $lifetime; + $this->assoc_type = $assoc_type; + } + + /** + * This returns the number of seconds this association is still + * valid for, or 0 if the association is no longer valid. + * + * @return integer $seconds The number of seconds this association + * is still valid for, or 0 if the association is no longer valid. + */ + function getExpiresIn($now = null) + { + if ($now == null) { + $now = time(); + } + + return max(0, $this->issued + $this->lifetime - $now); + } + + /** + * This checks to see if two {@link Auth_OpenID_Association} + * instances represent the same association. + * + * @return bool $result true if the two instances represent the + * same association, false otherwise. + */ + function equal($other) + { + return ((gettype($this) == gettype($other)) + && ($this->handle == $other->handle) + && ($this->secret == $other->secret) + && ($this->issued == $other->issued) + && ($this->lifetime == $other->lifetime) + && ($this->assoc_type == $other->assoc_type)); + } + + /** + * Convert an association to KV form. + * + * @return string $result String in KV form suitable for + * deserialization by deserialize. + */ + function serialize() + { + $data = array( + 'version' => '2', + 'handle' => $this->handle, + 'secret' => base64_encode($this->secret), + 'issued' => strval(intval($this->issued)), + 'lifetime' => strval(intval($this->lifetime)), + 'assoc_type' => $this->assoc_type + ); + + assert(array_keys($data) == $this->assoc_keys); + + return Auth_OpenID_KVForm::fromArray($data, $strict = true); + } + + /** + * Parse an association as stored by serialize(). This is the + * inverse of serialize. + * + * @param string $assoc_s Association as serialized by serialize() + * @return Auth_OpenID_Association $result instance of this class + */ + function deserialize($class_name, $assoc_s) + { + $pairs = Auth_OpenID_KVForm::toArray($assoc_s, $strict = true); + $keys = array(); + $values = array(); + foreach ($pairs as $key => $value) { + if (is_array($value)) { + list($key, $value) = $value; + } + $keys[] = $key; + $values[] = $value; + } + + $class_vars = get_class_vars($class_name); + $class_assoc_keys = $class_vars['assoc_keys']; + + sort($keys); + sort($class_assoc_keys); + + if ($keys != $class_assoc_keys) { + trigger_error('Unexpected key values: ' . var_export($keys, true), + E_USER_WARNING); + return null; + } + + $version = $pairs['version']; + $handle = $pairs['handle']; + $secret = $pairs['secret']; + $issued = $pairs['issued']; + $lifetime = $pairs['lifetime']; + $assoc_type = $pairs['assoc_type']; + + if ($version != '2') { + trigger_error('Unknown version: ' . $version, E_USER_WARNING); + return null; + } + + $issued = intval($issued); + $lifetime = intval($lifetime); + $secret = base64_decode($secret); + + return new $class_name( + $handle, $secret, $issued, $lifetime, $assoc_type); + } + + /** + * Generate a signature for a sequence of (key, value) pairs + * + * @access private + * @param array $pairs The pairs to sign, in order. This is an + * array of two-tuples. + * @return string $signature The binary signature of this sequence + * of pairs + */ + function sign($pairs) + { + $kv = Auth_OpenID_KVForm::fromArray($pairs); + + /* Invalid association types should be caught at constructor */ + $callback = $this->_macs[$this->assoc_type]; + + return call_user_func_array($callback, array($this->secret, $kv)); + } + + /** + * Generate a signature for some fields in a dictionary + * + * @access private + * @param array $fields The fields to sign, in order; this is an + * array of strings. + * @param array $data Dictionary of values to sign (an array of + * string => string pairs). + * @return string $signature The signature, base64 encoded + */ + function signMessage($message) + { + if ($message->hasKey(Auth_OpenID_OPENID_NS, 'sig') || + $message->hasKey(Auth_OpenID_OPENID_NS, 'signed')) { + // Already has a sig + return null; + } + + $extant_handle = $message->getArg(Auth_OpenID_OPENID_NS, + 'assoc_handle'); + + if ($extant_handle && ($extant_handle != $this->handle)) { + // raise ValueError("Message has a different association handle") + return null; + } + + $signed_message = $message; + $signed_message->setArg(Auth_OpenID_OPENID_NS, 'assoc_handle', + $this->handle); + + $message_keys = array_keys($signed_message->toPostArgs()); + $signed_list = array(); + $signed_prefix = 'openid.'; + + foreach ($message_keys as $k) { + if (strpos($k, $signed_prefix) === 0) { + $signed_list[] = substr($k, strlen($signed_prefix)); + } + } + + $signed_list[] = 'signed'; + sort($signed_list); + + $signed_message->setArg(Auth_OpenID_OPENID_NS, 'signed', + implode(',', $signed_list)); + $sig = $this->getMessageSignature($signed_message); + $signed_message->setArg(Auth_OpenID_OPENID_NS, 'sig', $sig); + return $signed_message; + } + + /** + * Given a {@link Auth_OpenID_Message}, return the key/value pairs + * to be signed according to the signed list in the message. If + * the message lacks a signed list, return null. + * + * @access private + */ + function _makePairs(&$message) + { + $signed = $message->getArg(Auth_OpenID_OPENID_NS, 'signed'); + if (!$signed || Auth_OpenID::isFailure($signed)) { + // raise ValueError('Message has no signed list: %s' % (message,)) + return null; + } + + $signed_list = explode(',', $signed); + $pairs = array(); + $data = $message->toPostArgs(); + foreach ($signed_list as $field) { + $pairs[] = array($field, Auth_OpenID::arrayGet($data, + 'openid.' . + $field, '')); + } + return $pairs; + } + + /** + * Given an {@link Auth_OpenID_Message}, return the signature for + * the signed list in the message. + * + * @access private + */ + function getMessageSignature(&$message) + { + $pairs = $this->_makePairs($message); + return base64_encode($this->sign($pairs)); + } + + /** + * Confirm that the signature of these fields matches the + * signature contained in the data. + * + * @access private + */ + function checkMessageSignature(&$message) + { + $sig = $message->getArg(Auth_OpenID_OPENID_NS, + 'sig'); + + if (!$sig || Auth_OpenID::isFailure($sig)) { + return false; + } + + $calculated_sig = $this->getMessageSignature($message); + return $calculated_sig == $sig; + } +} + +function Auth_OpenID_getSecretSize($assoc_type) +{ + if ($assoc_type == 'HMAC-SHA1') { + return 20; + } else if ($assoc_type == 'HMAC-SHA256') { + return 32; + } else { + return null; + } +} + +function Auth_OpenID_getAllAssociationTypes() +{ + return array('HMAC-SHA1', 'HMAC-SHA256'); +} + +function Auth_OpenID_getSupportedAssociationTypes() +{ + $a = array('HMAC-SHA1'); + + if (Auth_OpenID_HMACSHA256_SUPPORTED) { + $a[] = 'HMAC-SHA256'; + } + + return $a; +} + +function Auth_OpenID_getSessionTypes($assoc_type) +{ + $assoc_to_session = array( + 'HMAC-SHA1' => array('DH-SHA1', 'no-encryption')); + + if (Auth_OpenID_HMACSHA256_SUPPORTED) { + $assoc_to_session['HMAC-SHA256'] = + array('DH-SHA256', 'no-encryption'); + } + + return Auth_OpenID::arrayGet($assoc_to_session, $assoc_type, array()); +} + +function Auth_OpenID_checkSessionType($assoc_type, $session_type) +{ + if (!in_array($session_type, + Auth_OpenID_getSessionTypes($assoc_type))) { + return false; + } + + return true; +} + +function Auth_OpenID_getDefaultAssociationOrder() +{ + $order = array(); + + if (!Auth_OpenID_noMathSupport()) { + $order[] = array('HMAC-SHA1', 'DH-SHA1'); + + if (Auth_OpenID_HMACSHA256_SUPPORTED) { + $order[] = array('HMAC-SHA256', 'DH-SHA256'); + } + } + + $order[] = array('HMAC-SHA1', 'no-encryption'); + + if (Auth_OpenID_HMACSHA256_SUPPORTED) { + $order[] = array('HMAC-SHA256', 'no-encryption'); + } + + return $order; +} + +function Auth_OpenID_getOnlyEncryptedOrder() +{ + $result = array(); + + foreach (Auth_OpenID_getDefaultAssociationOrder() as $pair) { + list($assoc, $session) = $pair; + + if ($session != 'no-encryption') { + if (Auth_OpenID_HMACSHA256_SUPPORTED && + ($assoc == 'HMAC-SHA256')) { + $result[] = $pair; + } else if ($assoc != 'HMAC-SHA256') { + $result[] = $pair; + } + } + } + + return $result; +} + +function &Auth_OpenID_getDefaultNegotiator() +{ + $x = new Auth_OpenID_SessionNegotiator( + Auth_OpenID_getDefaultAssociationOrder()); + return $x; +} + +function &Auth_OpenID_getEncryptedNegotiator() +{ + $x = new Auth_OpenID_SessionNegotiator( + Auth_OpenID_getOnlyEncryptedOrder()); + return $x; +} + +/** + * A session negotiator controls the allowed and preferred association + * types and association session types. Both the {@link + * Auth_OpenID_Consumer} and {@link Auth_OpenID_Server} use + * negotiators when creating associations. + * + * You can create and use negotiators if you: + + * - Do not want to do Diffie-Hellman key exchange because you use + * transport-layer encryption (e.g. SSL) + * + * - Want to use only SHA-256 associations + * + * - Do not want to support plain-text associations over a non-secure + * channel + * + * It is up to you to set a policy for what kinds of associations to + * accept. By default, the library will make any kind of association + * that is allowed in the OpenID 2.0 specification. + * + * Use of negotiators in the library + * ================================= + * + * When a consumer makes an association request, it calls {@link + * getAllowedType} to get the preferred association type and + * association session type. + * + * The server gets a request for a particular association/session type + * and calls {@link isAllowed} to determine if it should create an + * association. If it is supported, negotiation is complete. If it is + * not, the server calls {@link getAllowedType} to get an allowed + * association type to return to the consumer. + * + * If the consumer gets an error response indicating that the + * requested association/session type is not supported by the server + * that contains an assocation/session type to try, it calls {@link + * isAllowed} to determine if it should try again with the given + * combination of association/session type. + * + * @package OpenID + */ +class Auth_OpenID_SessionNegotiator { + function Auth_OpenID_SessionNegotiator($allowed_types) + { + $this->allowed_types = array(); + $this->setAllowedTypes($allowed_types); + } + + /** + * Set the allowed association types, checking to make sure each + * combination is valid. + * + * @access private + */ + function setAllowedTypes($allowed_types) + { + foreach ($allowed_types as $pair) { + list($assoc_type, $session_type) = $pair; + if (!Auth_OpenID_checkSessionType($assoc_type, $session_type)) { + return false; + } + } + + $this->allowed_types = $allowed_types; + return true; + } + + /** + * Add an association type and session type to the allowed types + * list. The assocation/session pairs are tried in the order that + * they are added. + * + * @access private + */ + function addAllowedType($assoc_type, $session_type = null) + { + if ($this->allowed_types === null) { + $this->allowed_types = array(); + } + + if ($session_type === null) { + $available = Auth_OpenID_getSessionTypes($assoc_type); + + if (!$available) { + return false; + } + + foreach ($available as $session_type) { + $this->addAllowedType($assoc_type, $session_type); + } + } else { + if (Auth_OpenID_checkSessionType($assoc_type, $session_type)) { + $this->allowed_types[] = array($assoc_type, $session_type); + } else { + return false; + } + } + + return true; + } + + // Is this combination of association type and session type allowed? + function isAllowed($assoc_type, $session_type) + { + $assoc_good = in_array(array($assoc_type, $session_type), + $this->allowed_types); + + $matches = in_array($session_type, + Auth_OpenID_getSessionTypes($assoc_type)); + + return ($assoc_good && $matches); + } + + /** + * Get a pair of assocation type and session type that are + * supported. + */ + function getAllowedType() + { + if (!$this->allowed_types) { + return array(null, null); + } + + return $this->allowed_types[0]; + } +} + +?>
\ No newline at end of file diff --git a/Auth/OpenID/BigMath.php b/Auth/OpenID/BigMath.php new file mode 100644 index 0000000..4510494 --- /dev/null +++ b/Auth/OpenID/BigMath.php @@ -0,0 +1,471 @@ +<?php + +/** + * BigMath: A math library wrapper that abstracts out the underlying + * long integer library. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @access private + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Needed for random number generation + */ +require_once 'Auth/OpenID/CryptUtil.php'; + +/** + * Need Auth_OpenID::bytes(). + */ +require_once 'Auth/OpenID.php'; + +/** + * The superclass of all big-integer math implementations + * @access private + * @package OpenID + */ +class Auth_OpenID_MathLibrary { + /** + * Given a long integer, returns the number converted to a binary + * string. This function accepts long integer values of arbitrary + * magnitude and uses the local large-number math library when + * available. + * + * @param integer $long The long number (can be a normal PHP + * integer or a number created by one of the available long number + * libraries) + * @return string $binary The binary version of $long + */ + function longToBinary($long) + { + $cmp = $this->cmp($long, 0); + if ($cmp < 0) { + $msg = __FUNCTION__ . " takes only positive integers."; + trigger_error($msg, E_USER_ERROR); + return null; + } + + if ($cmp == 0) { + return "\x00"; + } + + $bytes = array(); + + while ($this->cmp($long, 0) > 0) { + array_unshift($bytes, $this->mod($long, 256)); + $long = $this->div($long, pow(2, 8)); + } + + if ($bytes && ($bytes[0] > 127)) { + array_unshift($bytes, 0); + } + + $string = ''; + foreach ($bytes as $byte) { + $string .= pack('C', $byte); + } + + return $string; + } + + /** + * Given a binary string, returns the binary string converted to a + * long number. + * + * @param string $binary The binary version of a long number, + * probably as a result of calling longToBinary + * @return integer $long The long number equivalent of the binary + * string $str + */ + function binaryToLong($str) + { + if ($str === null) { + return null; + } + + // Use array_merge to return a zero-indexed array instead of a + // one-indexed array. + $bytes = array_merge(unpack('C*', $str)); + + $n = $this->init(0); + + if ($bytes && ($bytes[0] > 127)) { + trigger_error("bytesToNum works only for positive integers.", + E_USER_WARNING); + return null; + } + + foreach ($bytes as $byte) { + $n = $this->mul($n, pow(2, 8)); + $n = $this->add($n, $byte); + } + + return $n; + } + + function base64ToLong($str) + { + $b64 = base64_decode($str); + + if ($b64 === false) { + return false; + } + + return $this->binaryToLong($b64); + } + + function longToBase64($str) + { + return base64_encode($this->longToBinary($str)); + } + + /** + * Returns a random number in the specified range. This function + * accepts $start, $stop, and $step values of arbitrary magnitude + * and will utilize the local large-number math library when + * available. + * + * @param integer $start The start of the range, or the minimum + * random number to return + * @param integer $stop The end of the range, or the maximum + * random number to return + * @param integer $step The step size, such that $result - ($step + * * N) = $start for some N + * @return integer $result The resulting randomly-generated number + */ + function rand($stop) + { + static $duplicate_cache = array(); + + // Used as the key for the duplicate cache + $rbytes = $this->longToBinary($stop); + + if (array_key_exists($rbytes, $duplicate_cache)) { + list($duplicate, $nbytes) = $duplicate_cache[$rbytes]; + } else { + if ($rbytes[0] == "\x00") { + $nbytes = Auth_OpenID::bytes($rbytes) - 1; + } else { + $nbytes = Auth_OpenID::bytes($rbytes); + } + + $mxrand = $this->pow(256, $nbytes); + + // If we get a number less than this, then it is in the + // duplicated range. + $duplicate = $this->mod($mxrand, $stop); + + if (count($duplicate_cache) > 10) { + $duplicate_cache = array(); + } + + $duplicate_cache[$rbytes] = array($duplicate, $nbytes); + } + + do { + $bytes = "\x00" . Auth_OpenID_CryptUtil::getBytes($nbytes); + $n = $this->binaryToLong($bytes); + // Keep looping if this value is in the low duplicated range + } while ($this->cmp($n, $duplicate) < 0); + + return $this->mod($n, $stop); + } +} + +/** + * Exposes BCmath math library functionality. + * + * {@link Auth_OpenID_BcMathWrapper} wraps the functionality provided + * by the BCMath extension. + * + * @access private + * @package OpenID + */ +class Auth_OpenID_BcMathWrapper extends Auth_OpenID_MathLibrary{ + var $type = 'bcmath'; + + function add($x, $y) + { + return bcadd($x, $y); + } + + function sub($x, $y) + { + return bcsub($x, $y); + } + + function pow($base, $exponent) + { + return bcpow($base, $exponent); + } + + function cmp($x, $y) + { + return bccomp($x, $y); + } + + function init($number, $base = 10) + { + return $number; + } + + function mod($base, $modulus) + { + return bcmod($base, $modulus); + } + + function mul($x, $y) + { + return bcmul($x, $y); + } + + function div($x, $y) + { + return bcdiv($x, $y); + } + + /** + * Same as bcpowmod when bcpowmod is missing + * + * @access private + */ + function _powmod($base, $exponent, $modulus) + { + $square = $this->mod($base, $modulus); + $result = 1; + while($this->cmp($exponent, 0) > 0) { + if ($this->mod($exponent, 2)) { + $result = $this->mod($this->mul($result, $square), $modulus); + } + $square = $this->mod($this->mul($square, $square), $modulus); + $exponent = $this->div($exponent, 2); + } + return $result; + } + + function powmod($base, $exponent, $modulus) + { + if (function_exists('bcpowmod')) { + return bcpowmod($base, $exponent, $modulus); + } else { + return $this->_powmod($base, $exponent, $modulus); + } + } + + function toString($num) + { + return $num; + } +} + +/** + * Exposes GMP math library functionality. + * + * {@link Auth_OpenID_GmpMathWrapper} wraps the functionality provided + * by the GMP extension. + * + * @access private + * @package OpenID + */ +class Auth_OpenID_GmpMathWrapper extends Auth_OpenID_MathLibrary{ + var $type = 'gmp'; + + function add($x, $y) + { + return gmp_add($x, $y); + } + + function sub($x, $y) + { + return gmp_sub($x, $y); + } + + function pow($base, $exponent) + { + return gmp_pow($base, $exponent); + } + + function cmp($x, $y) + { + return gmp_cmp($x, $y); + } + + function init($number, $base = 10) + { + return gmp_init($number, $base); + } + + function mod($base, $modulus) + { + return gmp_mod($base, $modulus); + } + + function mul($x, $y) + { + return gmp_mul($x, $y); + } + + function div($x, $y) + { + return gmp_div_q($x, $y); + } + + function powmod($base, $exponent, $modulus) + { + return gmp_powm($base, $exponent, $modulus); + } + + function toString($num) + { + return gmp_strval($num); + } +} + +/** + * Define the supported extensions. An extension array has keys + * 'modules', 'extension', and 'class'. 'modules' is an array of PHP + * module names which the loading code will attempt to load. These + * values will be suffixed with a library file extension (e.g. ".so"). + * 'extension' is the name of a PHP extension which will be tested + * before 'modules' are loaded. 'class' is the string name of a + * {@link Auth_OpenID_MathWrapper} subclass which should be + * instantiated if a given extension is present. + * + * You can define new math library implementations and add them to + * this array. + */ +function Auth_OpenID_math_extensions() +{ + $result = array(); + + if (!defined('Auth_OpenID_BUGGY_GMP')) { + $result[] = + array('modules' => array('gmp', 'php_gmp'), + 'extension' => 'gmp', + 'class' => 'Auth_OpenID_GmpMathWrapper'); + } + + $result[] = array( + 'modules' => array('bcmath', 'php_bcmath'), + 'extension' => 'bcmath', + 'class' => 'Auth_OpenID_BcMathWrapper'); + + return $result; +} + +/** + * Detect which (if any) math library is available + */ +function Auth_OpenID_detectMathLibrary($exts) +{ + $loaded = false; + + foreach ($exts as $extension) { + // See if the extension specified is already loaded. + if ($extension['extension'] && + extension_loaded($extension['extension'])) { + $loaded = true; + } + + // Try to load dynamic modules. + if (!$loaded) { + foreach ($extension['modules'] as $module) { + if (@dl($module . "." . PHP_SHLIB_SUFFIX)) { + $loaded = true; + break; + } + } + } + + // If the load succeeded, supply an instance of + // Auth_OpenID_MathWrapper which wraps the specified + // module's functionality. + if ($loaded) { + return $extension; + } + } + + return false; +} + +/** + * {@link Auth_OpenID_getMathLib} checks for the presence of long + * number extension modules and returns an instance of + * {@link Auth_OpenID_MathWrapper} which exposes the module's + * functionality. + * + * Checks for the existence of an extension module described by the + * result of {@link Auth_OpenID_math_extensions()} and returns an + * instance of a wrapper for that extension module. If no extension + * module is found, an instance of {@link Auth_OpenID_MathWrapper} is + * returned, which wraps the native PHP integer implementation. The + * proper calling convention for this method is $lib =& + * Auth_OpenID_getMathLib(). + * + * This function checks for the existence of specific long number + * implementations in the following order: GMP followed by BCmath. + * + * @return Auth_OpenID_MathWrapper $instance An instance of + * {@link Auth_OpenID_MathWrapper} or one of its subclasses + * + * @package OpenID + */ +function &Auth_OpenID_getMathLib() +{ + // The instance of Auth_OpenID_MathWrapper that we choose to + // supply will be stored here, so that subseqent calls to this + // method will return a reference to the same object. + static $lib = null; + + if (isset($lib)) { + return $lib; + } + + if (Auth_OpenID_noMathSupport()) { + $null = null; + return $null; + } + + // If this method has not been called before, look at + // Auth_OpenID_math_extensions and try to find an extension that + // works. + $ext = Auth_OpenID_detectMathLibrary(Auth_OpenID_math_extensions()); + if ($ext === false) { + $tried = array(); + foreach (Auth_OpenID_math_extensions() as $extinfo) { + $tried[] = $extinfo['extension']; + } + $triedstr = implode(", ", $tried); + + Auth_OpenID_setNoMathSupport(); + + $result = null; + return $result; + } + + // Instantiate a new wrapper + $class = $ext['class']; + $lib = new $class(); + + 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/Auth/OpenID/Consumer.php b/Auth/OpenID/Consumer.php new file mode 100644 index 0000000..a72684c --- /dev/null +++ b/Auth/OpenID/Consumer.php @@ -0,0 +1,2229 @@ +<?php + +/** + * This module documents the main interface with the OpenID consumer + * library. The only part of the library which has to be used and + * isn't documented in full here is the store required to create an + * Auth_OpenID_Consumer instance. More on the abstract store type and + * concrete implementations of it that are provided in the + * documentation for the Auth_OpenID_Consumer constructor. + * + * OVERVIEW + * + * The OpenID identity verification process most commonly uses the + * following steps, as visible to the user of this library: + * + * 1. The user enters their OpenID into a field on the consumer's + * site, and hits a login button. + * 2. The consumer site discovers the user's OpenID server using the + * YADIS protocol. + * 3. The consumer site sends the browser a redirect to the identity + * server. This is the authentication request as described in + * the OpenID specification. + * 4. The identity server's site sends the browser a redirect back + * to the consumer site. This redirect contains the server's + * response to the authentication request. + * + * The most important part of the flow to note is the consumer's site + * must handle two separate HTTP requests in order to perform the full + * identity check. + * + * LIBRARY DESIGN + * + * This consumer library is designed with that flow in mind. The goal + * is to make it as easy as possible to perform the above steps + * securely. + * + * At a high level, there are two important parts in the consumer + * library. The first important part is this module, which contains + * the interface to actually use this library. The second is the + * Auth_OpenID_Interface class, which describes the interface to use + * if you need to create a custom method for storing the state this + * library needs to maintain between requests. + * + * In general, the second part is less important for users of the + * library to know about, as several implementations are provided + * which cover a wide variety of situations in which consumers may use + * the library. + * + * This module contains a class, Auth_OpenID_Consumer, with methods + * corresponding to the actions necessary in each of steps 2, 3, and 4 + * described in the overview. Use of this library should be as easy + * as creating an Auth_OpenID_Consumer instance and calling the + * methods appropriate for the action the site wants to take. + * + * STORES AND DUMB MODE + * + * OpenID is a protocol that works best when the consumer site is able + * to store some state. This is the normal mode of operation for the + * protocol, and is sometimes referred to as smart mode. There is + * also a fallback mode, known as dumb mode, which is available when + * the consumer site is not able to store state. This mode should be + * avoided when possible, as it leaves the implementation more + * vulnerable to replay attacks. + * + * The mode the library works in for normal operation is determined by + * the store that it is given. The store is an abstraction that + * handles the data that the consumer needs to manage between http + * requests in order to operate efficiently and securely. + * + * Several store implementation are provided, and the interface is + * fully documented so that custom stores can be used as well. See + * the documentation for the Auth_OpenID_Consumer class for more + * information on the interface for stores. The implementations that + * are provided allow the consumer site to store the necessary data in + * several different ways, including several SQL databases and normal + * files on disk. + * + * There is an additional concrete store provided that puts the system + * in dumb mode. This is not recommended, as it removes the library's + * ability to stop replay attacks reliably. It still uses time-based + * checking to make replay attacks only possible within a small + * window, but they remain possible within that window. This store + * should only be used if the consumer site has no way to retain data + * between requests at all. + * + * IMMEDIATE MODE + * + * In the flow described above, the user may need to confirm to the + * lidentity server that it's ok to authorize his or her identity. + * The server may draw pages asking for information from the user + * before it redirects the browser back to the consumer's site. This + * is generally transparent to the consumer site, so it is typically + * ignored as an implementation detail. + * + * There can be times, however, where the consumer site wants to get a + * response immediately. When this is the case, the consumer can put + * the library in immediate mode. In immediate mode, there is an + * extra response possible from the server, which is essentially the + * server reporting that it doesn't have enough information to answer + * the question yet. + * + * USING THIS LIBRARY + * + * Integrating this library into an application is usually a + * relatively straightforward process. The process should basically + * follow this plan: + * + * Add an OpenID login field somewhere on your site. When an OpenID + * is entered in that field and the form is submitted, it should make + * a request to the your site which includes that OpenID URL. + * + * First, the application should instantiate the Auth_OpenID_Consumer + * class using the store of choice (Auth_OpenID_FileStore or one of + * the SQL-based stores). If the application has a custom + * session-management implementation, an object implementing the + * {@link Auth_Yadis_PHPSession} interface should be passed as the + * second parameter. Otherwise, the default uses $_SESSION. + * + * Next, the application should call the Auth_OpenID_Consumer object's + * 'begin' method. This method takes the OpenID URL. The 'begin' + * method returns an Auth_OpenID_AuthRequest object. + * + * Next, the application should call the 'redirectURL' method of the + * Auth_OpenID_AuthRequest object. The 'return_to' URL parameter is + * the URL that the OpenID server will send the user back to after + * attempting to verify his or her identity. The 'trust_root' is the + * URL (or URL pattern) that identifies your web site to the user when + * he or she is authorizing it. Send a redirect to the resulting URL + * to the user's browser. + * + * That's the first half of the authentication process. The second + * half of the process is done after the user's ID server sends the + * user's browser a redirect back to your site to complete their + * login. + * + * When that happens, the user will contact your site at the URL given + * as the 'return_to' URL to the Auth_OpenID_AuthRequest::redirectURL + * call made above. The request will have several query parameters + * added to the URL by the identity server as the information + * necessary to finish the request. + * + * Lastly, instantiate an Auth_OpenID_Consumer instance as above and + * call its 'complete' method, passing in all the received query + * arguments. + * + * There are multiple possible return types possible from that + * method. These indicate the whether or not the login was successful, + * and include any additional information appropriate for their type. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Require utility classes and functions for the consumer. + */ +require_once "Auth/OpenID.php"; +require_once "Auth/OpenID/Message.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"; + +/** + * This is the status code returned when the complete method returns + * successfully. + */ +define('Auth_OpenID_SUCCESS', 'success'); + +/** + * Status to indicate cancellation of OpenID authentication. + */ +define('Auth_OpenID_CANCEL', 'cancel'); + +/** + * This is the status code completeAuth returns when the value it + * received indicated an invalid login. + */ +define('Auth_OpenID_FAILURE', 'failure'); + +/** + * This is the status code completeAuth returns when the + * {@link Auth_OpenID_Consumer} instance is in immediate mode, and the + * identity server sends back a URL to send the user to to complete his + * or her login. + */ +define('Auth_OpenID_SETUP_NEEDED', 'setup needed'); + +/** + * This is the status code beginAuth returns when the page fetched + * from the entered OpenID URL doesn't contain the necessary link tags + * to function as an identity page. + */ +define('Auth_OpenID_PARSE_ERROR', 'parse error'); + +/** + * An OpenID consumer implementation that performs discovery and does + * session management. See the Consumer.php file documentation for + * more information. + * + * @package OpenID + */ +class Auth_OpenID_Consumer { + + /** + * @access private + */ + var $discoverMethod = 'Auth_OpenID_discover'; + + /** + * @access private + */ + var $session_key_prefix = "_openid_consumer_"; + + /** + * @access private + */ + var $_token_suffix = "last_token"; + + /** + * Initialize a Consumer instance. + * + * You should create a new instance of the Consumer object with + * every HTTP request that handles OpenID transactions. + * + * @param Auth_OpenID_OpenIDStore $store This must be an object + * that implements the interface in {@link + * Auth_OpenID_OpenIDStore}. Several concrete implementations are + * provided, to cover most common use cases. For stores backed by + * MySQL, PostgreSQL, or SQLite, see the {@link + * Auth_OpenID_SQLStore} class and its sublcasses. For a + * filesystem-backed store, see the {@link Auth_OpenID_FileStore} + * module. As a last resort, if it isn't possible for the server + * to store state at all, an instance of {@link + * Auth_OpenID_DumbStore} can be used. + * + * @param mixed $session An object which implements the interface + * of the {@link Auth_Yadis_PHPSession} class. Particularly, this + * object is expected to have these methods: get($key), set($key), + * $value), and del($key). This defaults to a session object + * which wraps PHP's native session machinery. You should only + * need to pass something here if you have your own sessioning + * implementation. + * + * @param str $consumer_cls The name of the class to instantiate + * when creating the internal consumer object. This is used for + * testing. + */ + function Auth_OpenID_Consumer(&$store, $session = null, + $consumer_cls = null) + { + if ($session === null) { + $session = new Auth_Yadis_PHPSession(); + } + + $this->session =& $session; + + if ($consumer_cls !== null) { + $this->consumer =& new $consumer_cls($store); + } else { + $this->consumer =& new Auth_OpenID_GenericConsumer($store); + } + + $this->_token_key = $this->session_key_prefix . $this->_token_suffix; + } + + /** + * Used in testing to define the discovery mechanism. + * + * @access private + */ + function getDiscoveryObject(&$session, $openid_url, + $session_key_prefix) + { + return new Auth_Yadis_Discovery($session, $openid_url, + $session_key_prefix); + } + + /** + * Start the OpenID authentication process. See steps 1-2 in the + * overview at the top of this file. + * + * @param string $user_url Identity URL given by the user. This + * method performs a textual transformation of the URL to try and + * make sure it is normalized. For example, a user_url of + * example.com will be normalized to http://example.com/ + * normalizing and resolving any redirects the server might issue. + * + * @param bool $anonymous True if the OpenID request is to be sent + * to the server without any identifier information. Use this + * when you want to transport data but don't want to do OpenID + * authentication with identifiers. + * + * @return Auth_OpenID_AuthRequest $auth_request An object + * containing the discovered information will be returned, with a + * method for building a redirect URL to the server, as described + * in step 3 of the overview. This object may also be used to add + * extension arguments to the request, using its 'addExtensionArg' + * method. + */ + function begin($user_url, $anonymous=false) + { + $openid_url = $user_url; + + $disco = $this->getDiscoveryObject($this->session, + $openid_url, + $this->session_key_prefix); + + // Set the 'stale' attribute of the manager. If discovery + // fails in a fatal way, the stale flag will cause the manager + // to be cleaned up next time discovery is attempted. + + $m = $disco->getManager(); + $loader = new Auth_Yadis_ManagerLoader(); + + if ($m) { + if ($m->stale) { + $disco->destroyManager(); + } else { + $m->stale = true; + $disco->session->set($disco->session_key, + serialize($loader->toSession($m))); + } + } + + $endpoint = $disco->getNextService($this->discoverMethod, + $this->consumer->fetcher); + + // Reset the 'stale' attribute of the manager. + $m =& $disco->getManager(); + if ($m) { + $m->stale = false; + $disco->session->set($disco->session_key, + serialize($loader->toSession($m))); + } + + if ($endpoint === null) { + return null; + } else { + return $this->beginWithoutDiscovery($endpoint, + $anonymous); + } + } + + /** + * Start OpenID verification without doing OpenID server + * discovery. This method is used internally by Consumer.begin + * after discovery is performed, and exists to provide an + * interface for library users needing to perform their own + * discovery. + * + * @param Auth_OpenID_ServiceEndpoint $endpoint an OpenID service + * endpoint descriptor. + * + * @param bool anonymous Set to true if you want to perform OpenID + * without identifiers. + * + * @return Auth_OpenID_AuthRequest $auth_request An OpenID + * authentication request object. + */ + function &beginWithoutDiscovery($endpoint, $anonymous=false) + { + $loader = new Auth_OpenID_ServiceEndpointLoader(); + $auth_req = $this->consumer->begin($endpoint); + $this->session->set($this->_token_key, + $loader->toSession($auth_req->endpoint)); + if (!$auth_req->setAnonymous($anonymous)) { + return new Auth_OpenID_FailureResponse(null, + "OpenID 1 requests MUST include the identifier " . + "in the request."); + } + return $auth_req; + } + + /** + * Called to interpret the server's response to an OpenID + * 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 + * PHP environment. It is only useful to override $query for + * testing. + * + * @return Auth_OpenID_ConsumerResponse $response A instance of an + * Auth_OpenID_ConsumerResponse subclass. The type of response is + * indicated by the status attribute, which will be one of + * SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED. + */ + 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(); + } + + $loader = new Auth_OpenID_ServiceEndpointLoader(); + $endpoint_data = $this->session->get($this->_token_key); + $endpoint = + $loader->fromSession($endpoint_data); + + $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))) { + if ($response->identity_url !== null) { + $disco = $this->getDiscoveryObject($this->session, + $response->identity_url, + $this->session_key_prefix); + $disco->cleanup(true); + } + } + + return $response; + } +} + +/** + * A class implementing HMAC/DH-SHA1 consumer sessions. + * + * @package OpenID + */ +class Auth_OpenID_DiffieHellmanSHA1ConsumerSession { + var $session_type = 'DH-SHA1'; + var $hash_func = 'Auth_OpenID_SHA1'; + var $secret_size = 20; + var $allowed_assoc_types = array('HMAC-SHA1'); + + function Auth_OpenID_DiffieHellmanSHA1ConsumerSession($dh = null) + { + if ($dh === null) { + $dh = new Auth_OpenID_DiffieHellman(); + } + + $this->dh = $dh; + } + + function getRequest() + { + $math =& Auth_OpenID_getMathLib(); + + $cpub = $math->longToBase64($this->dh->public); + + $args = array('dh_consumer_public' => $cpub); + + if (!$this->dh->usingDefaultValues()) { + $args = array_merge($args, array( + 'dh_modulus' => + $math->longToBase64($this->dh->mod), + 'dh_gen' => + $math->longToBase64($this->dh->gen))); + } + + return $args; + } + + function extractSecret($response) + { + if (!$response->hasKey(Auth_OpenID_OPENID_NS, + 'dh_server_public')) { + return null; + } + + if (!$response->hasKey(Auth_OpenID_OPENID_NS, + 'enc_mac_key')) { + return null; + } + + $math =& Auth_OpenID_getMathLib(); + + $spub = $math->base64ToLong($response->getArg(Auth_OpenID_OPENID_NS, + 'dh_server_public')); + $enc_mac_key = base64_decode($response->getArg(Auth_OpenID_OPENID_NS, + 'enc_mac_key')); + + return $this->dh->xorSecret($spub, $enc_mac_key, $this->hash_func); + } +} + +/** + * A class implementing HMAC/DH-SHA256 consumer sessions. + * + * @package OpenID + */ +class Auth_OpenID_DiffieHellmanSHA256ConsumerSession extends + Auth_OpenID_DiffieHellmanSHA1ConsumerSession { + var $session_type = 'DH-SHA256'; + var $hash_func = 'Auth_OpenID_SHA256'; + var $secret_size = 32; + var $allowed_assoc_types = array('HMAC-SHA256'); +} + +/** + * A class implementing plaintext consumer sessions. + * + * @package OpenID + */ +class Auth_OpenID_PlainTextConsumerSession { + var $session_type = 'no-encryption'; + var $allowed_assoc_types = array('HMAC-SHA1', 'HMAC-SHA256'); + + function getRequest() + { + return array(); + } + + function extractSecret($response) + { + if (!$response->hasKey(Auth_OpenID_OPENID_NS, 'mac_key')) { + return null; + } + + return base64_decode($response->getArg(Auth_OpenID_OPENID_NS, + 'mac_key')); + } +} + +/** + * Returns available session types. + */ +function Auth_OpenID_getAvailableSessionTypes() +{ + $types = array( + 'no-encryption' => 'Auth_OpenID_PlainTextConsumerSession', + 'DH-SHA1' => 'Auth_OpenID_DiffieHellmanSHA1ConsumerSession', + 'DH-SHA256' => 'Auth_OpenID_DiffieHellmanSHA256ConsumerSession'); + + return $types; +} + +/** + * This class is the interface to the OpenID consumer logic. + * Instances of it maintain no per-request state, so they can be + * reused (or even used by multiple threads concurrently) as needed. + * + * @package OpenID + */ +class Auth_OpenID_GenericConsumer { + /** + * @access private + */ + var $discoverMethod = 'Auth_OpenID_discover'; + + /** + * This consumer's store object. + */ + var $store; + + /** + * @access private + */ + var $_use_assocs; + + /** + * @access private + */ + 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. + * + * @param Auth_OpenID_OpenIDStore $store This must be an object + * that implements the interface in {@link Auth_OpenID_OpenIDStore}. + * Several concrete implementations are provided, to cover most common use + * cases. For stores backed by MySQL, PostgreSQL, or SQLite, see + * the {@link Auth_OpenID_SQLStore} class and its sublcasses. For a + * filesystem-backed store, see the {@link Auth_OpenID_FileStore} module. + * As a last resort, if it isn't possible for the server to store + * state at all, an instance of {@link Auth_OpenID_DumbStore} can be used. + * + * @param bool $immediate This is an optional boolean value. It + * controls whether the library uses immediate mode, as explained + * in the module description. The default value is False, which + * disables immediate mode. + */ + function Auth_OpenID_GenericConsumer(&$store) + { + $this->store =& $store; + $this->negotiator =& Auth_OpenID_getDefaultNegotiator(); + $this->_use_assocs = ($this->store ? true : false); + + $this->fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); + + $this->session_types = Auth_OpenID_getAvailableSessionTypes(); + } + + /** + * Called to begin OpenID authentication using the specified + * {@link Auth_OpenID_ServiceEndpoint}. + * + * @access private + */ + function begin($service_endpoint) + { + $assoc = $this->_getAssociation($service_endpoint); + $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; + } + + /** + * Given an {@link Auth_OpenID_Message}, {@link + * Auth_OpenID_ServiceEndpoint} and optional return_to URL, + * complete OpenID authentication. + * + * @access private + */ + function complete($message, $endpoint, $return_to) + { + $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode', + '<no mode set>'); + + $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); + } + + $user_setup_url = $message->getArg(Auth_OpenID_OPENID2_NS, + 'user_setup_url'); + return new Auth_OpenID_SetupNeededResponse($endpoint, $user_setup_url); + } + + /** + * @access private + */ + function _complete_id_res($message, &$endpoint, $return_to) + { + $user_setup_url = $message->getArg(Auth_OpenID_OPENID1_NS, + 'user_setup_url'); + + if ($this->_checkSetupNeeded($message)) { + return new Auth_OpenID_SetupNeededResponse( + $endpoint, $user_setup_url); + } else { + return $this->_doIdRes($message, $endpoint, $return_to); + } + } + + /** + * @access private + */ + function _checkSetupNeeded($message) + { + // In OpenID 1, we check to see if this is a cancel from + // immediate mode by the presence of the user_setup_url + // parameter. + if ($message->isOpenID1()) { + $user_setup_url = $message->getArg(Auth_OpenID_OPENID1_NS, + 'user_setup_url'); + if ($user_setup_url !== null) { + return true; + } + } + + return false; + } + + /** + * @access private + */ + function _doIdRes($message, $endpoint, $return_to) + { + // Checks for presence of appropriate fields (and checks + // signed list fields) + $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); + + if (Auth_OpenID::isFailure($result)) { + return $result; + } + + $endpoint = $result; + + $result = $this->_idResCheckSignature($message, + $endpoint->server_url); + + if (Auth_OpenID::isFailure($result)) { + return $result; + } + + $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, + $signed_fields); + + } + + /** + * @access private + */ + function _checkReturnTo($message, $return_to) + { + // Check an OpenID message and its openid.return_to value + // against a return_to URL from an application. Return True + // on success, False on failure. + + // Check the openid.return_to args against args in the + // original message. + $result = Auth_OpenID_GenericConsumer::_verifyReturnToArgs( + $message->toPostArgs()); + if (Auth_OpenID::isFailure($result)) { + return false; + } + + // Check the return_to base URL against the one in the + // 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(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. + if ((!array_key_exists('port', $return_to_parts)) && + (!array_key_exists('port', $msg_return_to_parts))) { + $return_to_parts['port'] = null; + $msg_return_to_parts['port'] = null; + } + + // If path is absent from both, add it so it's equal in the + // check below. + if ((!array_key_exists('path', $return_to_parts)) && + (!array_key_exists('path', $msg_return_to_parts))) { + $return_to_parts['path'] = null; + $msg_return_to_parts['path'] = null; + } + + // The URL scheme, authority, and path MUST be the same + // between the two URLs. + foreach (array('scheme', 'host', 'port', 'path') as $component) { + // If the url component is absent in either URL, fail. + // There should always be a scheme, host, port, and path. + if (!array_key_exists($component, $return_to_parts)) { + return false; + } + + if (!array_key_exists($component, $msg_return_to_parts)) { + return false; + } + + if (Auth_OpenID::arrayGet($return_to_parts, $component) !== + Auth_OpenID::arrayGet($msg_return_to_parts, $component)) { + return false; + } + } + + return true; + } + + /** + * @access private + */ + function _verifyReturnToArgs($query) + { + // Verify that the arguments in the return_to URL are present in this + // response. + + $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, + "Response has no return_to"); + } + + $parsed_url = parse_url($return_to); + + $q = array(); + if (array_key_exists('query', $parsed_url)) { + $rt_query = $parsed_url['query']; + $q = Auth_OpenID::parse_str($rt_query); + } + + foreach ($q as $rt_key => $rt_value) { + if (!array_key_exists($rt_key, $query)) { + return new Auth_OpenID_FailureResponse(null, + sprintf("return_to parameter %s absent from query", $rt_key)); + } else { + $value = $query[$rt_key]; + if ($rt_value != $value) { + return new Auth_OpenID_FailureResponse(null, + sprintf("parameter %s value %s does not match " . + "return_to value %s", $rt_key, + $value, $rt_value)); + } + } + } + + // 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; + } + + /** + * @access private + */ + function _idResCheckSignature($message, $server_url) + { + $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); + + if ($assoc) { + if ($assoc->getExpiresIn() <= 0) { + // XXX: It might be a good idea sometimes to re-start + // the authentication with a new association. Doing it + // automatically opens the possibility for + // denial-of-service by a server that just returns + // expired associations (or really short-lived + // associations) + return new Auth_OpenID_FailureResponse(null, + 'Association with ' . $server_url . ' expired'); + } + + if (!$assoc->checkMessageSignature($message)) { + return new Auth_OpenID_FailureResponse(null, + "Bad signature"); + } + } else { + // It's not an association we know about. Stateless mode + // is our only possible path for recovery. XXX - async + // framework will not want to block on this call to + // _checkAuth. + if (!$this->_checkAuth($message, $server_url)) { + return new Auth_OpenID_FailureResponse(null, + "Server denied check_authentication"); + } + } + + return null; + } + + /** + * @access private + */ + function _verifyDiscoveryResults($message, $endpoint=null) + { + if ($message->getOpenIDNamespace() == Auth_OpenID_OPENID2_NS) { + return $this->_verifyDiscoveryResultsOpenID2($message, + $endpoint); + } else { + return $this->_verifyDiscoveryResultsOpenID1($message, + $endpoint); + } + } + + /** + * @access private + */ + function _verifyDiscoveryResultsOpenID1($message, $endpoint) + { + $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(); + $to_match->type_uris = array(Auth_OpenID_TYPE_1_1); + $to_match->local_id = $message->getArg(Auth_OpenID_OPENID1_NS, + 'identity'); + + // Restore delegate information from the initiation phase + $to_match->claimed_id = $claimed_id; + + if ($to_match->local_id === null) { + return new Auth_OpenID_FailureResponse($endpoint, + "Missing required field openid.identity"); + } + + $to_match_1_0 = $to_match->copy(); + $to_match_1_0->type_uris = array(Auth_OpenID_TYPE_1_0); + + 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 (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)); + } + + /** + * @access private + */ + function _verifyDiscoverySingle($endpoint, $to_match) + { + // Every type URI that's in the to_match endpoint has to be + // present in the discovered endpoint. + foreach ($to_match->type_uris as $type_uri) { + if (!$endpoint->usesExtension($type_uri)) { + return new Auth_OpenID_TypeURIMismatch($endpoint, + "Required type ".$type_uri." not present"); + } + } + + // 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', $defragged_claimed_id, + $endpoint->claimed_id)); + } + + if ($to_match->getLocalID() != $endpoint->getLocalID()) { + return new Auth_OpenID_FailureResponse($endpoint, + sprintf('local_id mismatch. Expected %s, got %s', + $to_match->getLocalID(), $endpoint->getLocalID())); + } + + // If the server URL is None, this must be an OpenID 1 + // response, because op_endpoint is a required parameter in + // OpenID 2. In that case, we don't actually care what the + // discovered server_url is, because signature checking or + // check_auth should take care of that check for us. + if ($to_match->server_url === null) { + if ($to_match->preferredNamespace() != Auth_OpenID_OPENID1_NS) { + return new Auth_OpenID_FailureResponse($endpoint, + "Preferred namespace mismatch (bug)"); + } + } else if ($to_match->server_url != $endpoint->server_url) { + return new Auth_OpenID_FailureResponse($endpoint, + sprintf('OP Endpoint mismatch. Expected %s, got %s', + $to_match->server_url, $endpoint->server_url)); + } + + return null; + } + + /** + * @access private + */ + function _verifyDiscoveryResultsOpenID2($message, $endpoint) + { + $to_match = new Auth_OpenID_ServiceEndpoint(); + $to_match->type_uris = array(Auth_OpenID_TYPE_2_0); + $to_match->claimed_id = $message->getArg(Auth_OpenID_OPENID2_NS, + 'claimed_id'); + + $to_match->local_id = $message->getArg(Auth_OpenID_OPENID2_NS, + 'identity'); + + $to_match->server_url = $message->getArg(Auth_OpenID_OPENID2_NS, + 'op_endpoint'); + + if ($to_match->server_url === null) { + return new Auth_OpenID_FailureResponse($endpoint, + "OP Endpoint URL missing"); + } + + // claimed_id and identifier must both be present or both be + // absent + if (($to_match->claimed_id === null) && + ($to_match->local_id !== null)) { + return new Auth_OpenID_FailureResponse($endpoint, + 'openid.identity is present without openid.claimed_id'); + } + + 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'); + } + + 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); + } + + 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->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)) { + $endpoint = $this->_discoverAndVerify($to_match->claimed_id, + array($to_match)); + if (Auth_OpenID::isFailure($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; + } + + return $endpoint; + } + + /** + * @access private + */ + function _discoverAndVerify($claimed_id, $to_match_endpoints) + { + // oidutil.log('Performing discovery on %s' % (claimed_id,)) + list($unused, $services) = call_user_func($this->discoverMethod, + $claimed_id, + $this->fetcher); + + if (!$services) { + return new Auth_OpenID_FailureResponse(null, + sprintf("No OpenID information found at %s", + $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 + + foreach ($services as $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', + $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 $message->getArg(Auth_OpenID_BARE_NS, + $this->openid1_nonce_query_arg_name); + } + + /** + * @access private + */ + function _idResCheckNonce($message, $endpoint) + { + if ($message->isOpenID1()) { + // This indicates that the nonce was generated by the consumer + $nonce = $this->_idResGetNonceOpenID1($message, $endpoint); + $server_url = ''; + } else { + $nonce = $message->getArg(Auth_OpenID_OPENID2_NS, + 'response_nonce'); + + $server_url = $endpoint->server_url; + } + + if ($nonce === null) { + return new Auth_OpenID_FailureResponse($endpoint, + "Nonce missing from response"); + } + + $parts = Auth_OpenID_splitNonce($nonce); + + if ($parts === null) { + return new Auth_OpenID_FailureResponse($endpoint, + "Malformed nonce in response"); + } + + list($timestamp, $salt) = $parts; + + if (!$this->store->useNonce($server_url, $timestamp, $salt)) { + return new Auth_OpenID_FailureResponse($endpoint, + "Nonce already used or out of range"); + } + + return null; + } + + /** + * @access private + */ + function _idResCheckForFields($message) + { + $basic_fields = array('return_to', 'assoc_handle', 'sig', 'signed'); + $basic_sig_fields = array('return_to', 'identity'); + + $require_fields = array( + Auth_OpenID_OPENID2_NS => array_merge($basic_fields, + array('op_endpoint')), + + Auth_OpenID_OPENID1_NS => array_merge($basic_fields, + array('identity')) + ); + + $require_sigs = array( + Auth_OpenID_OPENID2_NS => array_merge($basic_sig_fields, + array('response_nonce', + 'claimed_id', + 'assoc_handle')), + Auth_OpenID_OPENID1_NS => array_merge($basic_sig_fields, + array('nonce')) + ); + + foreach ($require_fields[$message->getOpenIDNamespace()] as $field) { + if (!$message->hasKey(Auth_OpenID_OPENID_NS, $field)) { + return new Auth_OpenID_FailureResponse(null, + "Missing required field '".$field."'"); + } + } + + $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) && + (!in_array($field, $signed_list))) { + return new Auth_OpenID_FailureResponse(null, + "'".$field."' not signed"); + } + } + + return null; + } + + /** + * @access private + */ + function _checkAuth($message, $server_url) + { + $request = $this->_createCheckAuthRequest($message); + if ($request === null) { + return false; + } + + $resp_message = $this->_makeKVPost($request, $server_url); + if (($resp_message === null) || + (is_a($resp_message, 'Auth_OpenID_ServerErrorContainer'))) { + return false; + } + + return $this->_processCheckAuthResponse($resp_message, $server_url); + } + + /** + * @access private + */ + function _createCheckAuthRequest($message) + { + $signed = $message->getArg(Auth_OpenID_OPENID_NS, 'signed'); + if ($signed) { + foreach (explode(',', $signed) as $k) { + $value = $message->getAliasedArg($k); + if ($value === null) { + return null; + } + } + } + $ca_message = $message->copy(); + $ca_message->setArg(Auth_OpenID_OPENID_NS, 'mode', + 'check_authentication'); + return $ca_message; + } + + /** + * @access private + */ + function _processCheckAuthResponse($response, $server_url) + { + $is_valid = $response->getArg(Auth_OpenID_OPENID_NS, 'is_valid', + 'false'); + + $invalidate_handle = $response->getArg(Auth_OpenID_OPENID_NS, + 'invalidate_handle'); + + if ($invalidate_handle !== null) { + $this->store->removeAssociation($server_url, + $invalidate_handle); + } + + if ($is_valid == 'true') { + return true; + } + + 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 + */ + function _makeKVPost($message, $server_url) + { + $body = $message->toURLEncoded(); + $resp = $this->fetcher->post($server_url, $body); + + if ($resp === null) { + return null; + } + + return $this->_httpResponseToMessage($resp, $server_url); + } + + /** + * @access private + */ + function _getAssociation($endpoint) + { + if (!$this->_use_assocs) { + return null; + } + + $assoc = $this->store->getAssociation($endpoint->server_url); + + if (($assoc === null) || + ($assoc->getExpiresIn() <= 0)) { + + $assoc = $this->_negotiateAssociation($endpoint); + + if ($assoc !== null) { + $this->store->storeAssociation($endpoint->server_url, + $assoc); + } + } + + 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 + */ + function _negotiateAssociation($endpoint) + { + // Get our preferred session/association type from the negotiatior. + list($assoc_type, $session_type) = $this->negotiator->getAllowedType(); + + $assoc = $this->_requestAssociation( + $endpoint, $assoc_type, $session_type); + + if (Auth_OpenID::isFailure($assoc)) { + return null; + } + + if (is_a($assoc, 'Auth_OpenID_ServerErrorContainer')) { + $why = $assoc; + + $supportedTypes = $this->_extractSupportedAssociationType( + $why, $endpoint, $assoc_type); + + if ($supportedTypes !== null) { + list($assoc_type, $session_type) = $supportedTypes; + + // Attempt to create an association from the assoc_type + // and session_type that the server told us it + // supported. + $assoc = $this->_requestAssociation( + $endpoint, $assoc_type, $session_type); + + if (is_a($assoc, 'Auth_OpenID_ServerErrorContainer')) { + // Do not keep trying, since it rejected the + // association type that it told us to use. + // oidutil.log('Server %s refused its suggested association + // 'type: session_type=%s, assoc_type=%s' + // % (endpoint.server_url, session_type, + // assoc_type)) + return null; + } else { + return $assoc; + } + } else { + return null; + } + } else { + return $assoc; + } + } + + /** + * @access private + */ + function _requestAssociation($endpoint, $assoc_type, $session_type) + { + list($assoc_session, $args) = $this->_createAssociateRequest( + $endpoint, $assoc_type, $session_type); + + $response_message = $this->_makeKVPost($args, $endpoint->server_url); + + if ($response_message === null) { + // oidutil.log('openid.associate request failed: %s' % (why[0],)) + return null; + } else if (is_a($response_message, + 'Auth_OpenID_ServerErrorContainer')) { + return $response_message; + } + + return $this->_extractAssociation($response_message, $assoc_session); + } + + /** + * @access private + */ + function _extractAssociation(&$assoc_response, &$assoc_session) + { + // Extract the common fields from the response, raising an + // exception if they are not found + $assoc_type = $assoc_response->getArg( + Auth_OpenID_OPENID_NS, 'assoc_type', + Auth_OpenID_NO_DEFAULT); + + 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 (Auth_OpenID::isFailure($assoc_handle)) { + return $assoc_handle; + } + + // expires_in is a base-10 string. The Python parsing will + // accept literals that have whitespace around them and will + // accept negative values. Neither of these are really in-spec, + // but we think it's OK to accept them. + $expires_in_str = $assoc_response->getArg( + Auth_OpenID_OPENID_NS, 'expires_in', + Auth_OpenID_NO_DEFAULT); + + if (Auth_OpenID::isFailure($expires_in_str)) { + return $expires_in_str; + } + + $expires_in = Auth_OpenID::intval($expires_in_str); + if ($expires_in === false) { + + $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. + if ($assoc_response->isOpenID1()) { + $session_type = $this->_getOpenID1SessionType($assoc_response); + } else { + $session_type = $assoc_response->getArg( + Auth_OpenID_OPENID2_NS, 'session_type', + Auth_OpenID_NO_DEFAULT); + + if (Auth_OpenID::isFailure($session_type)) { + return $session_type; + } + } + + // Session type mismatch + if ($assoc_session->session_type != $session_type) { + if ($assoc_response->isOpenID1() && + ($session_type == 'no-encryption')) { + // In OpenID 1, any association request can result in + // a 'no-encryption' association response. Setting + // assoc_session to a new no-encryption session should + // make the rest of this function work properly for + // that case. + $assoc_session = new Auth_OpenID_PlainTextConsumerSession(); + } else { + // Any other mismatch, regardless of protocol version + // results in the failure of the association session + // altogether. + return null; + } + } + + // Make sure assoc_type is valid for session_type + if (!in_array($assoc_type, $assoc_session->allowed_assoc_types)) { + return null; + } + + // Delegate to the association session to extract the secret + // from the response, however is appropriate for that session + // type. + $secret = $assoc_session->extractSecret($assoc_response); + + if ($secret === null) { + return null; + } + + return Auth_OpenID_Association::fromExpiresIn( + $expires_in, $assoc_handle, $secret, $assoc_type); + } + + /** + * @access private + */ + function _createAssociateRequest($endpoint, $assoc_type, $session_type) + { + if (array_key_exists($session_type, $this->session_types)) { + $session_type_class = $this->session_types[$session_type]; + + if (is_callable($session_type_class)) { + $assoc_session = $session_type_class(); + } else { + $assoc_session = new $session_type_class(); + } + } else { + return null; + } + + $args = array( + 'mode' => 'associate', + 'assoc_type' => $assoc_type); + + if (!$endpoint->compatibilityMode()) { + $args['ns'] = Auth_OpenID_OPENID2_NS; + } + + // Leave out the session type if we're in compatibility mode + // *and* it's no-encryption. + if ((!$endpoint->compatibilityMode()) || + ($assoc_session->session_type != 'no-encryption')) { + $args['session_type'] = $assoc_session->session_type; + } + + $args = array_merge($args, $assoc_session->getRequest()); + $message = Auth_OpenID_Message::fromOpenIDArgs($args); + return array($assoc_session, $message); + } + + /** + * Given an association response message, extract the OpenID 1.X + * session type. + * + * This function mostly takes care of the 'no-encryption' default + * behavior in OpenID 1. + * + * If the association type is plain-text, this function will + * return 'no-encryption' + * + * @access private + * @return $typ The association type for this message + */ + function _getOpenID1SessionType($assoc_response) + { + // If it's an OpenID 1 message, allow session_type to default + // to None (which signifies "no-encryption") + $session_type = $assoc_response->getArg(Auth_OpenID_OPENID1_NS, + 'session_type'); + + // Handle the differences between no-encryption association + // respones in OpenID 1 and 2: + + // no-encryption is not really a valid session type for OpenID + // 1, but we'll accept it anyway, while issuing a warning. + if ($session_type == 'no-encryption') { + // oidutil.log('WARNING: OpenID server sent "no-encryption"' + // 'for OpenID 1.X') + } else if (($session_type == '') || ($session_type === null)) { + // Missing or empty session type is the way to flag a + // 'no-encryption' response. Change the session type to + // 'no-encryption' so that it can be handled in the same + // way as OpenID 2 'no-encryption' respones. + $session_type = 'no-encryption'; + } + + return $session_type; + } +} + +/** + * This class represents an authentication request from a consumer to + * an OpenID server. + * + * @package OpenID + */ +class Auth_OpenID_AuthRequest { + + /** + * Initialize an authentication request with the specified token, + * association, and endpoint. + * + * Users of this library should not create instances of this + * class. Instances of this class are created by the library when + * needed. + */ + function Auth_OpenID_AuthRequest(&$endpoint, $assoc) + { + $this->assoc = $assoc; + $this->endpoint =& $endpoint; + $this->return_to_args = array(); + $this->message = new Auth_OpenID_Message( + $endpoint->preferredNamespace()); + $this->_anonymous = false; + } + + /** + * Add an extension to this checkid request. + * + * $extension_request: An object that implements the extension + * request interface for adding arguments to an OpenID message. + */ + function addExtension(&$extension_request) + { + $extension_request->toMessage($this->message); + } + + /** + * Add an extension argument to this OpenID authentication + * request. + * + * Use caution when adding arguments, because they will be + * URL-escaped and appended to the redirect URL, which can easily + * get quite long. + * + * @param string $namespace The namespace for the extension. For + * example, the simple registration extension uses the namespace + * 'sreg'. + * + * @param string $key The key within the extension namespace. For + * example, the nickname field in the simple registration + * extension's key is 'nickname'. + * + * @param string $value The value to provide to the server for + * this argument. + */ + function addExtensionArg($namespace, $key, $value) + { + return $this->message->setArg($namespace, $key, $value); + } + + /** + * Set whether this request should be made anonymously. If a + * request is anonymous, the identifier will not be sent in the + * request. This is only useful if you are making another kind of + * request with an extension in this request. + * + * Anonymous requests are not allowed when the request is made + * with OpenID 1. + */ + function setAnonymous($is_anonymous) + { + if ($is_anonymous && $this->message->isOpenID1()) { + return false; + } else { + $this->_anonymous = $is_anonymous; + return true; + } + } + + /** + * Produce a {@link Auth_OpenID_Message} representing this + * request. + * + * @param string $realm The URL (or URL pattern) that identifies + * your web site to the user when she is authorizing it. + * + * @param string $return_to The URL that the OpenID provider will + * send the user back to after attempting to verify her identity. + * + * Not specifying a return_to URL means that the user will not be + * returned to the site issuing the request upon its completion. + * + * @param bool $immediate If true, the OpenID provider is to send + * back a response immediately, useful for behind-the-scenes + * authentication attempts. Otherwise the OpenID provider may + * engage the user before providing a response. This is the + * default case, as the user may need to provide credentials or + * approve the request before a positive response can be sent. + */ + function getMessage($realm, $return_to=null, $immediate=false) + { + if ($return_to) { + $return_to = Auth_OpenID::appendArgs($return_to, + $this->return_to_args); + } else if ($immediate) { + // raise ValueError( + // '"return_to" is mandatory when + //using "checkid_immediate"') + return new Auth_OpenID_FailureResponse(null, + "'return_to' is mandatory when using checkid_immediate"); + } else if ($this->message->isOpenID1()) { + // raise ValueError('"return_to" is + // mandatory for OpenID 1 requests') + return new Auth_OpenID_FailureResponse(null, + "'return_to' is mandatory for OpenID 1 requests"); + } else if ($this->return_to_args) { + // raise ValueError('extra "return_to" arguments + // were specified, but no return_to was specified') + return new Auth_OpenID_FailureResponse(null, + "extra 'return_to' arguments where specified, " . + "but no return_to was specified"); + } + + if ($immediate) { + $mode = 'checkid_immediate'; + } else { + $mode = 'checkid_setup'; + } + + $message = $this->message->copy(); + if ($message->isOpenID1()) { + $realm_key = 'trust_root'; + } else { + $realm_key = 'realm'; + } + + $message->updateArgs(Auth_OpenID_OPENID_NS, + array( + $realm_key => $realm, + 'mode' => $mode, + 'return_to' => $return_to)); + + if (!$this->_anonymous) { + if ($this->endpoint->isOPIdentifier()) { + // This will never happen when we're in compatibility + // mode, as long as isOPIdentifier() returns False + // whenever preferredNamespace() returns OPENID1_NS. + $claimed_id = $request_identity = + Auth_OpenID_IDENTIFIER_SELECT; + } else { + $request_identity = $this->endpoint->getLocalID(); + $claimed_id = $this->endpoint->claimed_id; + } + + // This is true for both OpenID 1 and 2 + $message->setArg(Auth_OpenID_OPENID_NS, 'identity', + $request_identity); + + if ($message->isOpenID2()) { + $message->setArg(Auth_OpenID_OPENID2_NS, 'claimed_id', + $claimed_id); + } + } + + if ($this->assoc) { + $message->setArg(Auth_OpenID_OPENID_NS, 'assoc_handle', + $this->assoc->handle); + } + + return $message; + } + + function redirectURL($realm, $return_to = null, + $immediate = false) + { + $message = $this->getMessage($realm, $return_to, $immediate); + + if (Auth_OpenID::isFailure($message)) { + return $message; + } + + return $message->toURL($this->endpoint->server_url); + } + + /** + * Get html for a form to submit this request to the IDP. + * + * form_tag_attrs: An array of attributes to be added to the form + * tag. 'accept-charset' and 'enctype' have defaults that can be + * overridden. If a value is supplied for 'action' or 'method', it + * will be replaced. + */ + function formMarkup($realm, $return_to=null, $immediate=false, + $form_tag_attrs=null) + { + $message = $this->getMessage($realm, $return_to, $immediate); + + if (Auth_OpenID::isFailure($message)) { + return $message; + } + + return $message->toFormMarkup($this->endpoint->server_url, + $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(); + } +} + +/** + * The base class for responses from the Auth_OpenID_Consumer. + * + * @package OpenID + */ +class Auth_OpenID_ConsumerResponse { + var $status = null; + + function setEndpoint($endpoint) + { + $this->endpoint = $endpoint; + if ($endpoint === null) { + $this->identity_url = null; + } else { + $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; + } +} + +/** + * A response with a status of Auth_OpenID_SUCCESS. Indicates that + * this request is a successful acknowledgement from the OpenID server + * that the supplied URL is, indeed controlled by the requesting + * agent. This has three relevant attributes: + * + * claimed_id - The identity URL that has been authenticated + * + * signed_args - The arguments in the server's response that were + * signed and verified. + * + * status - Auth_OpenID_SUCCESS. + * + * @package OpenID + */ +class Auth_OpenID_SuccessResponse extends Auth_OpenID_ConsumerResponse { + var $status = Auth_OpenID_SUCCESS; + + /** + * @access private + */ + function Auth_OpenID_SuccessResponse($endpoint, $message, $signed_args=null) + { + $this->endpoint = $endpoint; + $this->identity_url = $endpoint->claimed_id; + $this->signed_args = $signed_args; + $this->message = $message; + + if ($this->signed_args === null) { + $this->signed_args = array(); + } + } + + /** + * Extract signed extension data from the server's response. + * + * @param string $prefix The extension namespace from which to + * extract the extension data. + */ + function extensionResponse($namespace_uri, $require_signed) + { + if ($require_signed) { + return $this->getSignedNS($namespace_uri); + } else { + return $this->message->getArgs($namespace_uri); + } + } + + function isOpenID1() + { + return $this->message->isOpenID1(); + } + + function isSigned($ns_uri, $ns_key) + { + // Return whether a particular key is signed, regardless of + // its namespace alias + return in_array($this->message->getKey($ns_uri, $ns_key), + $this->signed_args); + } + + function getSigned($ns_uri, $ns_key, $default = null) + { + // Return the specified signed field if available, otherwise + // return default + if ($this->isSigned($ns_uri, $ns_key)) { + return $this->message->getArg($ns_uri, $ns_key, $default); + } else { + return $default; + } + } + + function getSignedNS($ns_uri) + { + $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)) { + return null; + } + } + + return $msg_args; + } + + /** + * Get the openid.return_to argument from this response. + * + * This is useful for verifying that this request was initiated by + * this consumer. + * + * @return string $return_to The return_to URL supplied to the + * server on the initial request, or null if the response did not + * contain an 'openid.return_to' argument. + */ + function getReturnTo() + { + return $this->getSigned(Auth_OpenID_OPENID_NS, 'return_to'); + } +} + +/** + * A response with a status of Auth_OpenID_FAILURE. Indicates that the + * OpenID protocol has failed. This could be locally or remotely + * triggered. This has three relevant attributes: + * + * claimed_id - The identity URL for which authentication was + * attempted, if it can be determined. Otherwise, null. + * + * message - A message indicating why the request failed, if one is + * supplied. Otherwise, null. + * + * status - Auth_OpenID_FAILURE. + * + * @package OpenID + */ +class Auth_OpenID_FailureResponse extends Auth_OpenID_ConsumerResponse { + var $status = Auth_OpenID_FAILURE; + + function Auth_OpenID_FailureResponse($endpoint, $message = null, + $contact = null, $reference = null) + { + $this->setEndpoint($endpoint); + $this->message = $message; + $this->contact = $contact; + $this->reference = $reference; + } +} + +/** + * A specific, internal failure used to detect type URI mismatch. + * + * @package OpenID + */ +class Auth_OpenID_TypeURIMismatch extends Auth_OpenID_FailureResponse { +} + +/** + * Exception that is raised when the server returns a 400 response + * code to a direct request. + * + * @package OpenID + */ +class Auth_OpenID_ServerErrorContainer { + function Auth_OpenID_ServerErrorContainer($error_text, + $error_code, + $message) + { + $this->error_text = $error_text; + $this->error_code = $error_code; + $this->message = $message; + } + + /** + * @access private + */ + function fromMessage($message) + { + $error_text = $message->getArg( + Auth_OpenID_OPENID_NS, 'error', '<no error message supplied>'); + $error_code = $message->getArg(Auth_OpenID_OPENID_NS, 'error_code'); + return new Auth_OpenID_ServerErrorContainer($error_text, + $error_code, + $message); + } +} + +/** + * A response with a status of Auth_OpenID_CANCEL. Indicates that the + * user cancelled the OpenID authentication request. This has two + * relevant attributes: + * + * claimed_id - The identity URL for which authentication was + * attempted, if it can be determined. Otherwise, null. + * + * status - Auth_OpenID_SUCCESS. + * + * @package OpenID + */ +class Auth_OpenID_CancelResponse extends Auth_OpenID_ConsumerResponse { + var $status = Auth_OpenID_CANCEL; + + function Auth_OpenID_CancelResponse($endpoint) + { + $this->setEndpoint($endpoint); + } +} + +/** + * A response with a status of Auth_OpenID_SETUP_NEEDED. Indicates + * that the request was in immediate mode, and the server is unable to + * authenticate the user without further interaction. + * + * claimed_id - The identity URL for which authentication was + * attempted. + * + * setup_url - A URL that can be used to send the user to the server + * to set up for authentication. The user should be redirected in to + * the setup_url, either in the current window or in a new browser + * window. Null in OpenID 2. + * + * status - Auth_OpenID_SETUP_NEEDED. + * + * @package OpenID + */ +class Auth_OpenID_SetupNeededResponse extends Auth_OpenID_ConsumerResponse { + var $status = Auth_OpenID_SETUP_NEEDED; + + function Auth_OpenID_SetupNeededResponse($endpoint, + $setup_url = null) + { + $this->setEndpoint($endpoint); + $this->setup_url = $setup_url; + } +} + +?> diff --git a/Auth/OpenID/CryptUtil.php b/Auth/OpenID/CryptUtil.php new file mode 100644 index 0000000..aacc3cd --- /dev/null +++ b/Auth/OpenID/CryptUtil.php @@ -0,0 +1,109 @@ +<?php + +/** + * CryptUtil: A suite of wrapper utility functions for the OpenID + * library. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @access private + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +if (!defined('Auth_OpenID_RAND_SOURCE')) { + /** + * The filename for a source of random bytes. Define this yourself + * if you have a different source of randomness. + */ + define('Auth_OpenID_RAND_SOURCE', '/dev/urandom'); +} + +class Auth_OpenID_CryptUtil { + /** + * Get the specified number of random bytes. + * + * Attempts to use a cryptographically secure (not predictable) + * source of randomness if available. If there is no high-entropy + * randomness source available, it will fail. As a last resort, + * for non-critical systems, define + * <code>Auth_OpenID_RAND_SOURCE</code> as <code>null</code>, and + * the code will fall back on a pseudo-random number generator. + * + * @param int $num_bytes The length of the return value + * @return string $bytes random bytes + */ + function getBytes($num_bytes) + { + static $f = null; + $bytes = ''; + if ($f === null) { + if (Auth_OpenID_RAND_SOURCE === null) { + $f = false; + } else { + $f = @fopen(Auth_OpenID_RAND_SOURCE, "r"); + if ($f === false) { + $msg = 'Define Auth_OpenID_RAND_SOURCE as null to ' . + ' continue with an insecure random number generator.'; + trigger_error($msg, E_USER_ERROR); + } + } + } + if ($f === false) { + // pseudorandom used + $bytes = ''; + for ($i = 0; $i < $num_bytes; $i += 4) { + $bytes .= pack('L', mt_rand()); + } + $bytes = substr($bytes, 0, $num_bytes); + } else { + $bytes = fread($f, $num_bytes); + } + return $bytes; + } + + /** + * Produce a string of length random bytes, chosen from chrs. If + * $chrs is null, the resulting string may contain any characters. + * + * @param integer $length The length of the resulting + * randomly-generated string + * @param string $chrs A string of characters from which to choose + * to build the new string + * @return string $result A string of randomly-chosen characters + * from $chrs + */ + function randomString($length, $population = null) + { + if ($population === null) { + return Auth_OpenID_CryptUtil::getBytes($length); + } + + $popsize = strlen($population); + + if ($popsize > 256) { + $msg = 'More than 256 characters supplied to ' . __FUNCTION__; + trigger_error($msg, E_USER_ERROR); + } + + $duplicate = 256 % $popsize; + + $str = ""; + for ($i = 0; $i < $length; $i++) { + do { + $n = ord(Auth_OpenID_CryptUtil::getBytes(1)); + } while ($n < $duplicate); + + $n %= $popsize; + $str .= $population[$n]; + } + + return $str; + } +} + +?>
\ No newline at end of file diff --git a/Auth/OpenID/DatabaseConnection.php b/Auth/OpenID/DatabaseConnection.php new file mode 100644 index 0000000..9db6e0e --- /dev/null +++ b/Auth/OpenID/DatabaseConnection.php @@ -0,0 +1,131 @@ +<?php + +/** + * The Auth_OpenID_DatabaseConnection class, which is used to emulate + * a PEAR database connection. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * An empty base class intended to emulate PEAR connection + * functionality in applications that supply their own database + * abstraction mechanisms. See {@link Auth_OpenID_SQLStore} for more + * information. You should subclass this class if you need to create + * an SQL store that needs to access its database using an + * application's database abstraction layer instead of a PEAR database + * connection. Any subclass of Auth_OpenID_DatabaseConnection MUST + * adhere to the interface specified here. + * + * @package OpenID + */ +class Auth_OpenID_DatabaseConnection { + /** + * Sets auto-commit mode on this database connection. + * + * @param bool $mode True if auto-commit is to be used; false if + * not. + */ + function autoCommit($mode) + { + } + + /** + * Run an SQL query with the specified parameters, if any. + * + * @param string $sql An SQL string with placeholders. The + * placeholders are assumed to be specific to the database engine + * for this connection. + * + * @param array $params An array of parameters to insert into the + * SQL string using this connection's escaping mechanism. + * + * @return mixed $result The result of calling this connection's + * internal query function. The type of result depends on the + * underlying database engine. This method is usually used when + * the result of a query is not important, like a DDL query. + */ + function query($sql, $params = array()) + { + } + + /** + * Starts a transaction on this connection, if supported. + */ + function begin() + { + } + + /** + * Commits a transaction on this connection, if supported. + */ + function commit() + { + } + + /** + * Performs a rollback on this connection, if supported. + */ + function rollback() + { + } + + /** + * Run an SQL query and return the first column of the first row + * of the result set, if any. + * + * @param string $sql An SQL string with placeholders. The + * placeholders are assumed to be specific to the database engine + * for this connection. + * + * @param array $params An array of parameters to insert into the + * SQL string using this connection's escaping mechanism. + * + * @return mixed $result The value of the first column of the + * first row of the result set. False if no such result was + * found. + */ + function getOne($sql, $params = array()) + { + } + + /** + * Run an SQL query and return the first row of the result set, if + * any. + * + * @param string $sql An SQL string with placeholders. The + * placeholders are assumed to be specific to the database engine + * for this connection. + * + * @param array $params An array of parameters to insert into the + * SQL string using this connection's escaping mechanism. + * + * @return array $result The first row of the result set, if any, + * keyed on column name. False if no such result was found. + */ + function getRow($sql, $params = array()) + { + } + + /** + * Run an SQL query with the specified parameters, if any. + * + * @param string $sql An SQL string with placeholders. The + * placeholders are assumed to be specific to the database engine + * for this connection. + * + * @param array $params An array of parameters to insert into the + * SQL string using this connection's escaping mechanism. + * + * @return array $result An array of arrays representing the + * result of the query; each array is keyed on column name. + */ + function getAll($sql, $params = array()) + { + } +} + +?>
\ No newline at end of file diff --git a/Auth/OpenID/DiffieHellman.php b/Auth/OpenID/DiffieHellman.php new file mode 100644 index 0000000..f4ded7e --- /dev/null +++ b/Auth/OpenID/DiffieHellman.php @@ -0,0 +1,113 @@ +<?php + +/** + * The OpenID library's Diffie-Hellman implementation. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @access private + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @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'; + +function Auth_OpenID_getDefaultMod() +{ + return '155172898181473697471232257763715539915724801'. + '966915404479707795314057629378541917580651227423'. + '698188993727816152646631438561595825688188889951'. + '272158842675419950341258706556549803580104870537'. + '681476726513255747040765857479291291572334510643'. + '245094715007229621094194349783925984760375594985'. + '848253359305585439638443'; +} + +function Auth_OpenID_getDefaultGen() +{ + return '2'; +} + +/** + * The Diffie-Hellman key exchange class. This class relies on + * {@link Auth_OpenID_MathLibrary} to perform large number operations. + * + * @access private + * @package OpenID + */ +class Auth_OpenID_DiffieHellman { + + var $mod; + var $gen; + var $private; + var $lib = null; + + function Auth_OpenID_DiffieHellman($mod = null, $gen = null, + $private = null, $lib = null) + { + if ($lib === null) { + $this->lib =& Auth_OpenID_getMathLib(); + } else { + $this->lib =& $lib; + } + + if ($mod === null) { + $this->mod = $this->lib->init(Auth_OpenID_getDefaultMod()); + } else { + $this->mod = $mod; + } + + if ($gen === null) { + $this->gen = $this->lib->init(Auth_OpenID_getDefaultGen()); + } else { + $this->gen = $gen; + } + + if ($private === null) { + $r = $this->lib->rand($this->mod); + $this->private = $this->lib->add($r, 1); + } else { + $this->private = $private; + } + + $this->public = $this->lib->powmod($this->gen, $this->private, + $this->mod); + } + + function getSharedSecret($composite) + { + return $this->lib->powmod($composite, $this->private, $this->mod); + } + + function getPublicKey() + { + return $this->public; + } + + function usingDefaultValues() + { + return ($this->mod == Auth_OpenID_getDefaultMod() && + $this->gen == Auth_OpenID_getDefaultGen()); + } + + function xorSecret($composite, $secret, $hash_func) + { + $dh_shared = $this->getSharedSecret($composite); + $dh_shared_str = $this->lib->longToBinary($dh_shared); + $hash_dh_shared = $hash_func($dh_shared_str); + + $xsecret = ""; + for ($i = 0; $i < Auth_OpenID::bytes($secret); $i++) { + $xsecret .= chr(ord($secret[$i]) ^ ord($hash_dh_shared[$i])); + } + + return $xsecret; + } +} + +?> diff --git a/Auth/OpenID/Discover.php b/Auth/OpenID/Discover.php new file mode 100644 index 0000000..62aeb1d --- /dev/null +++ b/Auth/OpenID/Discover.php @@ -0,0 +1,548 @@ +<?php + +/** + * The OpenID and Yadis discovery implementation for OpenID 1.2. + */ + +require_once "Auth/OpenID.php"; +require_once "Auth/OpenID/Parse.php"; +require_once "Auth/OpenID/Message.php"; +require_once "Auth/Yadis/XRIRes.php"; +require_once "Auth/Yadis/Yadis.php"; + +// XML namespace value +define('Auth_OpenID_XMLNS_1_0', 'http://openid.net/xmlns/1.0'); + +// Yadis service types +define('Auth_OpenID_TYPE_1_2', 'http://openid.net/signon/1.2'); +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() +{ + return array(Auth_OpenID_TYPE_2_0_IDP, + Auth_OpenID_TYPE_2_0, + Auth_OpenID_TYPE_1_2, + Auth_OpenID_TYPE_1_1, + Auth_OpenID_TYPE_1_0, + Auth_OpenID_RP_RETURN_TO_URL_TYPE); +} + +/** + * Object representing an OpenID service endpoint. + */ +class Auth_OpenID_ServiceEndpoint { + function Auth_OpenID_ServiceEndpoint() + { + $this->claimed_id = null; + $this->server_url = null; + $this->type_uris = array(); + $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) + { + return in_array($extension_uri, $this->type_uris); + } + + function preferredNamespace() + { + if (in_array(Auth_OpenID_TYPE_2_0_IDP, $this->type_uris) || + in_array(Auth_OpenID_TYPE_2_0, $this->type_uris)) { + return Auth_OpenID_OPENID2_NS; + } else { + return Auth_OpenID_OPENID1_NS; + } + } + + /* + * 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? + return ((in_array($type_uri, $this->type_uris)) || + (($type_uri == Auth_OpenID_TYPE_2_0) && + $this->isOPIdentifier())); + } + + function compatibilityMode() + { + return $this->preferredNamespace() != Auth_OpenID_OPENID2_NS; + } + + function isOPIdentifier() + { + return in_array(Auth_OpenID_TYPE_2_0_IDP, $this->type_uris); + } + + function fromOPEndpointURL($op_endpoint_url) + { + // Construct an OP-Identifier OpenIDServiceEndpoint object for + // a given OP Endpoint URL + $obj = new Auth_OpenID_ServiceEndpoint(); + $obj->server_url = $op_endpoint_url; + $obj->type_uris = array(Auth_OpenID_TYPE_2_0_IDP); + return $obj; + } + + function parseService($yadis_url, $uri, $type_uris, $service_element) + { + // Set the state of this object based on the contents of the + // service element. Return true if successful, false if not + // (if findOPLocalIdentifier returns false). + $this->type_uris = $type_uris; + $this->server_url = $uri; + $this->used_yadis = true; + + if (!$this->isOPIdentifier()) { + $this->claimed_id = $yadis_url; + $this->local_id = Auth_OpenID_findOPLocalIdentifier( + $service_element, + $this->type_uris); + if ($this->local_id === false) { + return false; + } + } + + return true; + } + + function getLocalID() + { + // Return the identifier that should be sent as the + // openid.identity_url parameter to the server. + if ($this->local_id === null && $this->canonicalID === null) { + return $this->claimed_id; + } else { + if ($this->local_id) { + return $this->local_id; + } else { + return $this->canonicalID; + } + } + } + + /* + * 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( + array(Auth_OpenID_TYPE_2_0, + 'openid2.provider', 'openid2.local_id'), + array(Auth_OpenID_TYPE_1_1, + 'openid.server', 'openid.delegate') + ); + + $services = array(); + + foreach ($discovery_types as $triple) { + list($type_uri, $server_rel, $delegate_rel) = $triple; + + $urls = Auth_OpenID_legacy_discover($html, $server_rel, + $delegate_rel); + + if ($urls === false) { + continue; + } + + list($delegate_url, $server_url) = $urls; + + $service = new Auth_OpenID_ServiceEndpoint(); + $service->claimed_id = $uri; + $service->local_id = $delegate_url; + $service->server_url = $server_url; + $service->type_uris = array($type_uri); + + $services[] = $service; + } + + return $services; + } + + function copy() + { + $x = new Auth_OpenID_ServiceEndpoint(); + + $x->claimed_id = $this->claimed_id; + $x->server_url = $this->server_url; + $x->type_uris = $this->type_uris; + $x->local_id = $this->local_id; + $x->canonicalID = $this->canonicalID; + $x->used_yadis = $this->used_yadis; + + return $x; + } +} + +function Auth_OpenID_findOPLocalIdentifier($service, $type_uris) +{ + // Extract a openid:Delegate value from a Yadis Service element. + // If no delegate is found, returns null. Returns false on + // discovery failure (when multiple delegate/localID tags have + // different values). + + $service->parser->registerNamespace('openid', + Auth_OpenID_XMLNS_1_0); + + $service->parser->registerNamespace('xrd', + Auth_Yadis_XMLNS_XRD_2_0); + + $parser =& $service->parser; + + $permitted_tags = array(); + + if (in_array(Auth_OpenID_TYPE_1_1, $type_uris) || + in_array(Auth_OpenID_TYPE_1_0, $type_uris)) { + $permitted_tags[] = 'openid:Delegate'; + } + + if (in_array(Auth_OpenID_TYPE_2_0, $type_uris)) { + $permitted_tags[] = 'xrd:LocalID'; + } + + $local_id = null; + + foreach ($permitted_tags as $tag_name) { + $tags = $service->getElements($tag_name); + + foreach ($tags as $tag) { + $content = $parser->content($tag); + + if ($local_id === null) { + $local_id = $content; + } else if ($local_id != $content) { + return false; + } + } + } + + return $local_id; +} + +function filter_MatchesAnyOpenIDType(&$service) +{ + $uris = $service->getTypes(); + + foreach ($uris as $uri) { + if (in_array($uri, Auth_OpenID_getOpenIDTypeURIs())) { + return true; + } + } + + return false; +} + +function Auth_OpenID_bestMatchingService($service, $preferred_types) +{ + // Return the index of the first matching type, or something + // higher if no type matches. + // + // This provides an ordering in which service elements that + // contain a type that comes earlier in the preferred types list + // come before service elements that come later. If a service + // element has more than one type, the most preferred one wins. + + foreach ($preferred_types as $index => $typ) { + if (in_array($typ, $service->type_uris)) { + return $index; + } + } + + return count($preferred_types); +} + +function Auth_OpenID_arrangeByType($service_list, $preferred_types) +{ + // Rearrange service_list in a new list so services are ordered by + // types listed in preferred_types. Return the new list. + + // Build a list with the service elements in tuples whose + // comparison will prefer the one with the best matching service + $prio_services = array(); + foreach ($service_list as $index => $service) { + $prio_services[] = array(Auth_OpenID_bestMatchingService($service, + $preferred_types), + $index, $service); + } + + sort($prio_services); + + // Now that the services are sorted by priority, remove the sort + // keys from the list. + foreach ($prio_services as $index => $s) { + $prio_services[$index] = $prio_services[$index][2]; + } + + return $prio_services; +} + +// Extract OP Identifier services. If none found, return the rest, +// sorted with most preferred first according to +// OpenIDServiceEndpoint.openid_type_uris. +// +// openid_services is a list of OpenIDServiceEndpoint objects. +// +// Returns a list of OpenIDServiceEndpoint objects.""" +function Auth_OpenID_getOPOrUserServices($openid_services) +{ + $op_services = Auth_OpenID_arrangeByType($openid_services, + array(Auth_OpenID_TYPE_2_0_IDP)); + + $openid_services = Auth_OpenID_arrangeByType($openid_services, + Auth_OpenID_getOpenIDTypeURIs()); + + if ($op_services) { + return $op_services; + } else { + return $openid_services; + } +} + +function Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services) +{ + $s = array(); + + if (!$yadis_services) { + return $s; + } + + foreach ($yadis_services as $service) { + $type_uris = $service->getTypes(); + $uris = $service->getURIs(); + + // If any Type URIs match and there is an endpoint URI + // specified, then this is an OpenID endpoint + if ($type_uris && + $uris) { + foreach ($uris as $service_uri) { + $openid_endpoint = new Auth_OpenID_ServiceEndpoint(); + if ($openid_endpoint->parseService($uri, + $service_uri, + $type_uris, + $service)) { + $s[] = $openid_endpoint; + } + } + } + } + + return $s; +} + +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. + + // Might raise a yadis.discover.DiscoveryFailure if no document + // 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 = call_user_func_array($discover_function, + array($uri, &$fetcher)); + + $yadis_url = $response->normalized_uri; + $yadis_services = array(); + + if ($response->isFailure()) { + return array($uri, array()); + } + + $openid_services = Auth_OpenID_ServiceEndpoint::fromXRDS( + $yadis_url, + $response->response_text); + + if (!$openid_services) { + if ($response->isXRDS()) { + return Auth_OpenID_discoverWithoutYadis($uri, + $fetcher); + } + + // Try to parse the response as HTML to get OpenID 1.0/1.1 + // <link rel="..."> + $openid_services = Auth_OpenID_ServiceEndpoint::fromHTML( + $yadis_url, + $response->response_text); + } + + $openid_services = call_user_func_array($endpoint_filter, + array(&$openid_services)); + + return array($yadis_url, $openid_services); +} + +function Auth_OpenID_discoverURI($uri, &$fetcher) +{ + $uri = Auth_OpenID::normalizeUrl($uri); + return Auth_OpenID_discoverWithYadis($uri, $fetcher); +} + +function Auth_OpenID_discoverWithoutYadis($uri, &$fetcher) +{ + $http_resp = @$fetcher->get($uri); + + if ($http_resp->status != 200 and $http_resp->status != 206) { + return array($uri, array()); + } + + $identity_url = $http_resp->final_url; + + // Try to parse the response as HTML to get OpenID 1.0/1.1 <link + // rel="..."> + $openid_services = Auth_OpenID_ServiceEndpoint::fromHTML( + $identity_url, + $http_resp->body); + + return array($identity_url, $openid_services); +} + +function Auth_OpenID_discoverXRI($iname, &$fetcher) +{ + $resolver = new Auth_Yadis_ProxyResolver($fetcher); + list($canonicalID, $yadis_services) = + $resolver->query($iname, + Auth_OpenID_getOpenIDTypeURIs(), + array('filter_MatchesAnyOpenIDType')); + + $openid_services = Auth_OpenID_makeOpenIDEndpoints($iname, + $yadis_services); + + $openid_services = Auth_OpenID_getOPOrUserServices($openid_services); + + 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 + return array($iname, $openid_services); +} + +function Auth_OpenID_discover($uri, &$fetcher) +{ + // If the fetcher (i.e., PHP) doesn't support SSL, we can't do + // discovery on an HTTPS URL. + if ($fetcher->isHTTPS($uri) && !$fetcher->supportsSSL()) { + return array($uri, array()); + } + + if (Auth_Yadis_identifierScheme($uri) == 'XRI') { + $result = Auth_OpenID_discoverXRI($uri, $fetcher); + } else { + $result = Auth_OpenID_discoverURI($uri, $fetcher); + } + + // If the fetcher doesn't support SSL, we can't interact with + // HTTPS server URLs; remove those endpoints from the list. + if (!$fetcher->supportsSSL()) { + $http_endpoints = array(); + list($new_uri, $endpoints) = $result; + + foreach ($endpoints as $e) { + if (!$fetcher->isHTTPS($e->server_url)) { + $http_endpoints[] = $e; + } + } + + $result = array($new_uri, $http_endpoints); + } + + return $result; +} + +?> diff --git a/Auth/OpenID/DumbStore.php b/Auth/OpenID/DumbStore.php new file mode 100644 index 0000000..22fd2d3 --- /dev/null +++ b/Auth/OpenID/DumbStore.php @@ -0,0 +1,100 @@ +<?php + +/** + * This file supplies a dumb store backend for OpenID servers and + * consumers. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @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/HMAC.php'; + +/** + * This is a store for use in the worst case, when you have no way of + * saving state on the consumer site. Using this store makes the + * consumer vulnerable to replay attacks, as it's unable to use + * nonces. Avoid using this store if it is at all possible. + * + * Most of the methods of this class are implementation details. + * Users of this class need to worry only about the constructor. + * + * @package OpenID + */ +class Auth_OpenID_DumbStore extends Auth_OpenID_OpenIDStore { + + /** + * Creates a new {@link Auth_OpenID_DumbStore} instance. For the security + * of the tokens generated by the library, this class attempts to + * at least have a secure implementation of getAuthKey. + * + * When you create an instance of this class, pass in a secret + * phrase. The phrase is hashed with sha1 to make it the correct + * length and form for an auth key. That allows you to use a long + * string as the secret phrase, which means you can make it very + * difficult to guess. + * + * Each {@link Auth_OpenID_DumbStore} instance that is created for use by + * your consumer site needs to use the same $secret_phrase. + * + * @param string secret_phrase The phrase used to create the auth + * key returned by getAuthKey + */ + function Auth_OpenID_DumbStore($secret_phrase) + { + $this->auth_key = Auth_OpenID_SHA1($secret_phrase); + } + + /** + * This implementation does nothing. + */ + function storeAssociation($server_url, $association) + { + } + + /** + * This implementation always returns null. + */ + function getAssociation($server_url, $handle = null) + { + return null; + } + + /** + * This implementation always returns false. + */ + function removeAssociation($server_url, $handle) + { + return false; + } + + /** + * In a system truly limited to dumb mode, nonces must all be + * accepted. This therefore always returns true, which makes + * replay attacks feasible. + */ + function useNonce($server_url, $timestamp, $salt) + { + return true; + } + + /** + * This method returns the auth key generated by the constructor. + */ + function getAuthKey() + { + return $this->auth_key; + } +} + +?>
\ No newline at end of file diff --git a/Auth/OpenID/Extension.php b/Auth/OpenID/Extension.php new file mode 100644 index 0000000..f362a4b --- /dev/null +++ b/Auth/OpenID/Extension.php @@ -0,0 +1,62 @@ +<?php + +/** + * An interface for OpenID extensions. + * + * @package OpenID + */ + +/** + * Require the Message implementation. + */ +require_once 'Auth/OpenID/Message.php'; + +/** + * A base class for accessing extension request and response data for + * the OpenID 2 protocol. + * + * @package OpenID + */ +class Auth_OpenID_Extension { + /** + * ns_uri: The namespace to which to add the arguments for this + * extension + */ + var $ns_uri = null; + var $ns_alias = null; + + /** + * Get the string arguments that should be added to an OpenID + * message for this extension. + */ + function getExtensionArgs() + { + return null; + } + + /** + * Add the arguments from this extension to the provided message. + * + * Returns the message with the extension arguments added. + */ + function toMessage(&$message) + { + $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; + } + } + + $message->updateArgs($this->ns_uri, + $this->getExtensionArgs()); + return $message; + } +} + +?>
\ No newline at end of file diff --git a/Auth/OpenID/FileStore.php b/Auth/OpenID/FileStore.php new file mode 100644 index 0000000..29d8d20 --- /dev/null +++ b/Auth/OpenID/FileStore.php @@ -0,0 +1,618 @@ +<?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 JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Require base class for creating a new interface. + */ +require_once 'Auth/OpenID.php'; +require_once 'Auth/OpenID/Interface.php'; +require_once 'Auth/OpenID/HMAC.php'; +require_once 'Auth/OpenID/Nonce.php'; + +/** + * This is a filesystem-based store for OpenID associations and + * nonces. This store should be safe for use in concurrent systems on + * both windows and unix (excluding NFS filesystems). There are a + * couple race conditions in the system, but those failure cases have + * been set up in such a way that the worst-case behavior is someone + * having to try to log in a second time. + * + * 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_FileStore extends Auth_OpenID_OpenIDStore { + + /** + * Initializes a new {@link Auth_OpenID_FileStore}. This + * initializes the nonce and association directories, which are + * subdirectories of the directory passed in. + * + * @param string $directory This is the directory to put the store + * directories in. + */ + function Auth_OpenID_FileStore($directory) + { + if (!Auth_OpenID::ensureDir($directory)) { + trigger_error('Not a directory and failed to create: ' + . $directory, E_USER_ERROR); + } + $directory = realpath($directory); + + $this->directory = $directory; + $this->active = true; + + $this->nonce_dir = $directory . DIRECTORY_SEPARATOR . 'nonces'; + + $this->association_dir = $directory . DIRECTORY_SEPARATOR . + 'associations'; + + // Temp dir must be on the same filesystem as the assciations + // $directory. + $this->temp_dir = $directory . DIRECTORY_SEPARATOR . 'temp'; + + $this->max_nonce_age = 6 * 60 * 60; // Six hours, in seconds + + if (!$this->_setup()) { + trigger_error('Failed to initialize OpenID file store in ' . + $directory, E_USER_ERROR); + } + } + + function destroy() + { + Auth_OpenID_FileStore::_rmtree($this->directory); + $this->active = false; + } + + /** + * Make sure that the directories in which we store our data + * exist. + * + * @access private + */ + function _setup() + { + return (Auth_OpenID::ensureDir($this->nonce_dir) && + Auth_OpenID::ensureDir($this->association_dir) && + Auth_OpenID::ensureDir($this->temp_dir)); + } + + /** + * Create a temporary file on the same filesystem as + * $this->association_dir. + * + * The temporary directory should not be cleaned if there are any + * processes using the store. If there is no active process using + * the store, it is safe to remove all of the files in the + * temporary directory. + * + * @return array ($fd, $filename) + * @access private + */ + function _mktemp() + { + $name = Auth_OpenID_FileStore::_mkstemp($dir = $this->temp_dir); + $file_obj = @fopen($name, 'wb'); + if ($file_obj !== false) { + return array($file_obj, $name); + } else { + Auth_OpenID_FileStore::_removeIfPresent($name); + } + } + + 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 + * format of the handle. The filename that is returned will + * contain the domain name from the server URL for ease of human + * inspection of the data directory. + * + * @return string $filename + */ + function getAssociationFilename($server_url, $handle) + { + if (!$this->active) { + trigger_error("FileStore no longer active", E_USER_ERROR); + return null; + } + + if (strpos($server_url, '://') === false) { + trigger_error(sprintf("Bad server URL: %s", $server_url), + E_USER_WARNING); + return null; + } + + list($proto, $rest) = explode('://', $server_url, 2); + $parts = explode('/', $rest); + $domain = Auth_OpenID_FileStore::_filenameEscape($parts[0]); + $url_hash = Auth_OpenID_FileStore::_safe64($server_url); + if ($handle) { + $handle_hash = Auth_OpenID_FileStore::_safe64($handle); + } else { + $handle_hash = ''; + } + + $filename = sprintf('%s-%s-%s-%s', $proto, $domain, $url_hash, + $handle_hash); + + return $this->association_dir. DIRECTORY_SEPARATOR . $filename; + } + + /** + * Store an association in the association directory. + */ + function storeAssociation($server_url, $association) + { + if (!$this->active) { + trigger_error("FileStore no longer active", E_USER_ERROR); + return false; + } + + $association_s = $association->serialize(); + $filename = $this->getAssociationFilename($server_url, + $association->handle); + list($tmp_file, $tmp) = $this->_mktemp(); + + if (!$tmp_file) { + trigger_error("_mktemp didn't return a valid file descriptor", + E_USER_WARNING); + return false; + } + + fwrite($tmp_file, $association_s); + + fflush($tmp_file); + + fclose($tmp_file); + + if (@rename($tmp, $filename)) { + return true; + } else { + // In case we are running on Windows, try unlinking the + // file in case it exists. + @unlink($filename); + + // Now the target should not exist. Try renaming again, + // giving up if it fails. + if (@rename($tmp, $filename)) { + return true; + } + } + + // If there was an error, don't leave the temporary file + // around. + Auth_OpenID_FileStore::_removeIfPresent($tmp); + return false; + } + + /** + * Retrieve an association. If no handle is specified, return the + * association with the most recent issue time. + * + * @return mixed $association + */ + function getAssociation($server_url, $handle = null) + { + if (!$this->active) { + trigger_error("FileStore no longer active", E_USER_ERROR); + return null; + } + + if ($handle === null) { + $handle = ''; + } + + // The filename with the empty handle is a prefix of all other + // associations for the given server URL. + $filename = $this->getAssociationFilename($server_url, $handle); + + if ($handle) { + return $this->_getAssociation($filename); + } else { + $association_files = + Auth_OpenID_FileStore::_listdir($this->association_dir); + $matching_files = array(); + + // strip off the path to do the comparison + $name = basename($filename); + foreach ($association_files as $association_file) { + $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 $full_name) { + $association = $this->_getAssociation($full_name); + if ($association !== null) { + $matching_associations[] = array($association->issued, + $association); + } + } + + $issued = array(); + $assocs = array(); + foreach ($matching_associations as $key => $assoc) { + $issued[$key] = $assoc[0]; + $assocs[$key] = $assoc[1]; + } + + array_multisort($issued, SORT_DESC, $assocs, SORT_DESC, + $matching_associations); + + // return the most recently issued one. + if ($matching_associations) { + list($issued, $assoc) = $matching_associations[0]; + return $assoc; + } else { + return null; + } + } + } + + /** + * @access private + */ + function _getAssociation($filename) + { + if (!$this->active) { + trigger_error("FileStore no longer active", E_USER_ERROR); + return null; + } + + $assoc_file = @fopen($filename, 'rb'); + + if ($assoc_file === false) { + return null; + } + + $assoc_s = fread($assoc_file, filesize($filename)); + fclose($assoc_file); + + if (!$assoc_s) { + return null; + } + + $association = + Auth_OpenID_Association::deserialize('Auth_OpenID_Association', + $assoc_s); + + if (!$association) { + Auth_OpenID_FileStore::_removeIfPresent($filename); + return null; + } + + if ($association->getExpiresIn() == 0) { + Auth_OpenID_FileStore::_removeIfPresent($filename); + return null; + } else { + return $association; + } + } + + /** + * Remove an association if it exists. Do nothing if it does not. + * + * @return bool $success + */ + function removeAssociation($server_url, $handle) + { + if (!$this->active) { + trigger_error("FileStore no longer active", E_USER_ERROR); + return null; + } + + $assoc = $this->getAssociation($server_url, $handle); + if ($assoc === null) { + return false; + } else { + $filename = $this->getAssociationFilename($server_url, $handle); + return Auth_OpenID_FileStore::_removeIfPresent($filename); + } + } + + /** + * Return whether this nonce is present. As a side effect, mark it + * as no longer present. + * + * @return bool $present + */ + 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 { + $proto = ''; + $rest = ''; + } + + $parts = explode('/', $rest, 2); + $domain = $this->_filenameEscape($parts[0]); + $url_hash = $this->_safe64($server_url); + $salt_hash = $this->_safe64($salt); + + $filename = sprintf('%08x-%s-%s-%s-%s', $timestamp, $proto, + $domain, $url_hash, $salt_hash); + $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $filename; + + $result = @fopen($filename, 'x'); + + if ($result === false) { + return false; + } else { + fclose($result); + return true; + } + } + + /** + * Remove expired entries from the database. This is potentially + * expensive, so only run when it is acceptable to take time. + * + * @access private + */ + function _allAssocs() + { + $all_associations = array(); + + $association_filenames = + Auth_OpenID_FileStore::_listdir($this->association_dir); + + foreach ($association_filenames as $association_filename) { + $association_file = fopen($association_filename, 'rb'); + + if ($association_file !== false) { + $assoc_s = fread($association_file, + filesize($association_filename)); + fclose($association_file); + + // Remove expired or corrupted associations + $association = + Auth_OpenID_Association::deserialize( + 'Auth_OpenID_Association', $assoc_s); + + if ($association === null) { + Auth_OpenID_FileStore::_removeIfPresent( + $association_filename); + } else { + if ($association->getExpiresIn() == 0) { + $all_associations[] = array($association_filename, + $association); + } + } + } + } + + return $all_associations; + } + + function clean() + { + if (!$this->active) { + trigger_error("FileStore no longer active", E_USER_ERROR); + return null; + } + + $nonces = Auth_OpenID_FileStore::_listdir($this->nonce_dir); + $now = time(); + + // Check all nonces for expiry + foreach ($nonces as $nonce) { + if (!Auth_OpenID_checkTimestamp($nonce, $now)) { + $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $nonce; + Auth_OpenID_FileStore::_removeIfPresent($filename); + } + } + + foreach ($this->_allAssocs() as $pair) { + list($assoc_filename, $assoc) = $pair; + if ($assoc->getExpiresIn() == 0) { + Auth_OpenID_FileStore::_removeIfPresent($assoc_filename); + } + } + } + + /** + * @access private + */ + function _rmtree($dir) + { + if ($dir[strlen($dir) - 1] != DIRECTORY_SEPARATOR) { + $dir .= DIRECTORY_SEPARATOR; + } + + if ($handle = opendir($dir)) { + while ($item = readdir($handle)) { + if (!in_array($item, array('.', '..'))) { + if (is_dir($dir . $item)) { + + if (!Auth_OpenID_FileStore::_rmtree($dir . $item)) { + return false; + } + } else if (is_file($dir . $item)) { + if (!unlink($dir . $item)) { + return false; + } + } + } + } + + closedir($handle); + + if (!@rmdir($dir)) { + return false; + } + + return true; + } else { + // Couldn't open directory. + return false; + } + } + + /** + * @access private + */ + function _mkstemp($dir) + { + foreach (range(0, 4) as $i) { + $name = tempnam($dir, "php_openid_filestore_"); + + if ($name !== false) { + return $name; + } + } + return false; + } + + /** + * @access private + */ + function _mkdtemp($dir) + { + foreach (range(0, 4) as $i) { + $name = $dir . strval(DIRECTORY_SEPARATOR) . strval(getmypid()) . + "-" . strval(rand(1, time())); + if (!mkdir($name, 0700)) { + return false; + } else { + return $name; + } + } + return false; + } + + /** + * @access private + */ + function _listdir($dir) + { + $handle = opendir($dir); + $files = array(); + while (false !== ($filename = readdir($handle))) { + if (!in_array($filename, array('.', '..'))) { + $files[] = $dir . DIRECTORY_SEPARATOR . $filename; + } + } + return $files; + } + + /** + * @access private + */ + function _isFilenameSafe($char) + { + $_Auth_OpenID_filename_allowed = Auth_OpenID_letters . + Auth_OpenID_digits . "."; + return (strpos($_Auth_OpenID_filename_allowed, $char) !== false); + } + + /** + * @access private + */ + function _safe64($str) + { + $h64 = base64_encode(Auth_OpenID_SHA1($str)); + $h64 = str_replace('+', '_', $h64); + $h64 = str_replace('/', '.', $h64); + $h64 = str_replace('=', '', $h64); + return $h64; + } + + /** + * @access private + */ + function _filenameEscape($str) + { + $filename = ""; + $b = Auth_OpenID::toBytes($str); + + for ($i = 0; $i < count($b); $i++) { + $c = $b[$i]; + if (Auth_OpenID_FileStore::_isFilenameSafe($c)) { + $filename .= $c; + } else { + $filename .= sprintf("_%02X", ord($c)); + } + } + return $filename; + } + + /** + * Attempt to remove a file, returning whether the file existed at + * the time of the call. + * + * @access private + * @return bool $result True if the file was present, false if not. + */ + function _removeIfPresent($filename) + { + 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/Auth/OpenID/HMAC.php b/Auth/OpenID/HMAC.php new file mode 100644 index 0000000..ec42db8 --- /dev/null +++ b/Auth/OpenID/HMAC.php @@ -0,0 +1,99 @@ +<?php + +/** + * This is the HMACSHA1 implementation for the OpenID library. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @access private + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'Auth/OpenID.php'; + +/** + * SHA1_BLOCKSIZE is this module's SHA1 blocksize used by the fallback + * implementation. + */ +define('Auth_OpenID_SHA1_BLOCKSIZE', 64); + +function Auth_OpenID_SHA1($text) +{ + if (function_exists('hash') && + function_exists('hash_algos') && + (in_array('sha1', hash_algos()))) { + // PHP 5 case (sometimes): 'hash' available and 'sha1' algo + // supported. + return hash('sha1', $text, true); + } else if (function_exists('sha1')) { + // PHP 4 case: 'sha1' available. + $hex = sha1($text); + $raw = ''; + for ($i = 0; $i < 40; $i += 2) { + $hexcode = substr($hex, $i, 2); + $charcode = (int)base_convert($hexcode, 16, 10); + $raw .= chr($charcode); + } + return $raw; + } else { + // Explode. + trigger_error('No SHA1 function found', E_USER_ERROR); + } +} + +/** + * Compute an HMAC/SHA1 hash. + * + * @access private + * @param string $key The HMAC key + * @param string $text The message text to hash + * @return string $mac The MAC + */ +function Auth_OpenID_HMACSHA1($key, $text) +{ + if (Auth_OpenID::bytes($key) > Auth_OpenID_SHA1_BLOCKSIZE) { + $key = Auth_OpenID_SHA1($key, true); + } + + $key = str_pad($key, Auth_OpenID_SHA1_BLOCKSIZE, chr(0x00)); + $ipad = str_repeat(chr(0x36), Auth_OpenID_SHA1_BLOCKSIZE); + $opad = str_repeat(chr(0x5c), Auth_OpenID_SHA1_BLOCKSIZE); + $hash1 = Auth_OpenID_SHA1(($key ^ $ipad) . $text, true); + $hmac = Auth_OpenID_SHA1(($key ^ $opad) . $hash1, true); + return $hmac; +} + +if (function_exists('hash') && + function_exists('hash_algos') && + (in_array('sha256', hash_algos()))) { + function Auth_OpenID_SHA256($text) + { + // PHP 5 case: 'hash' available and 'sha256' algo supported. + return hash('sha256', $text, true); + } + define('Auth_OpenID_SHA256_SUPPORTED', true); +} else { + define('Auth_OpenID_SHA256_SUPPORTED', false); +} + +if (function_exists('hash_hmac') && + function_exists('hash_algos') && + (in_array('sha256', hash_algos()))) { + + function Auth_OpenID_HMACSHA256($key, $text) + { + // Return raw MAC (not hex string). + return hash_hmac('sha256', $text, $key, true); + } + + define('Auth_OpenID_HMACSHA256_SUPPORTED', true); +} else { + define('Auth_OpenID_HMACSHA256_SUPPORTED', false); +} + +?>
\ No newline at end of file diff --git a/Auth/OpenID/Interface.php b/Auth/OpenID/Interface.php new file mode 100644 index 0000000..f4c6062 --- /dev/null +++ b/Auth/OpenID/Interface.php @@ -0,0 +1,197 @@ +<?php + +/** + * This file specifies the interface for PHP OpenID store implementations. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * This is the interface for the store objects the OpenID library + * uses. It is a single class that provides all of the persistence + * mechanisms that the OpenID library needs, for both servers and + * 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, 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> + */ +class Auth_OpenID_OpenIDStore { + /** + * This method puts an Association object into storage, + * retrievable by server URL and handle. + * + * @param string $server_url The URL of the identity server that + * this association is with. Because of the way the server portion + * of the library uses this interface, don't assume there are any + * limitations on the character set of the input string. In + * particular, expect to see unescaped non-url-safe characters in + * the server_url field. + * + * @param Association $association The Association to store. + */ + function storeAssociation($server_url, $association) + { + trigger_error("Auth_OpenID_OpenIDStore::storeAssociation ". + "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 + * null if no such association is found or if the matching + * association is expired. + * + * 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 + * most recently issued. + * + * This method is allowed (and encouraged) to garbage collect + * expired associations when found. This method must not return + * expired associations. + * + * @param string $server_url The URL of the identity server to get + * the association for. Because of the way the server portion of + * the library uses this interface, don't assume there are any + * limitations on the character set of the input string. In + * particular, expect to see unescaped non-url-safe characters in + * the server_url field. + * + * @param mixed $handle This optional parameter is the handle of + * the specific association to get. If no specific handle is + * provided, any valid association matching the server URL is + * returned. + * + * @return Association The Association for the given identity + * server. + */ + function getAssociation($server_url, $handle = null) + { + trigger_error("Auth_OpenID_OpenIDStore::getAssociation ". + "not implemented", E_USER_ERROR); + } + + /** + * This method removes the matching association if it's found, and + * returns whether the association was removed or not. + * + * @param string $server_url The URL of the identity server the + * association to remove belongs to. Because of the way the server + * portion of the library uses this interface, don't assume there + * are any limitations on the character set of the input + * string. In particular, expect to see unescaped non-url-safe + * characters in the server_url field. + * + * @param string $handle This is the handle of the association to + * remove. If there isn't an association found that matches both + * the given URL and handle, then there was no matching handle + * found. + * + * @return mixed Returns whether or not the given association existed. + */ + function removeAssociation($server_url, $handle) + { + trigger_error("Auth_OpenID_OpenIDStore::removeAssociation ". + "not implemented", E_USER_ERROR); + } + + /** + * Called when using a nonce. + * + * This method should return C{True} if the nonce has not been + * used before, and store it for a while to make sure nobody + * tries to use the same value again. If the nonce has already + * been used, return C{False}. + * + * Change: In earlier versions, round-trip nonces were used and a + * nonce was only valid if it had been previously stored with + * storeNonce. Version 2.0 uses one-way nonces, requiring a + * different implementation here that does not depend on a + * storeNonce call. (storeNonce is no longer part of the + * interface. + * + * @param string $nonce The nonce to use. + * + * @return bool Whether or not the nonce was valid. + */ + function useNonce($server_url, $timestamp, $salt) + { + trigger_error("Auth_OpenID_OpenIDStore::useNonce ". + "not implemented", E_USER_ERROR); + } + + /** + * Removes all entries from the store; implementation is optional. + */ + function reset() + { + } + +} +?>
\ No newline at end of file diff --git a/Auth/OpenID/KVForm.php b/Auth/OpenID/KVForm.php new file mode 100644 index 0000000..fb342a0 --- /dev/null +++ b/Auth/OpenID/KVForm.php @@ -0,0 +1,112 @@ +<?php + +/** + * OpenID protocol key-value/comma-newline format parsing and + * serialization + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @access private + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Container for key-value/comma-newline OpenID format and parsing + */ +class Auth_OpenID_KVForm { + /** + * Convert an OpenID colon/newline separated string into an + * associative array + * + * @static + * @access private + */ + function toArray($kvs, $strict=false) + { + $lines = explode("\n", $kvs); + + $last = array_pop($lines); + if ($last !== '') { + array_push($lines, $last); + if ($strict) { + return false; + } + } + + $values = array(); + + for ($lineno = 0; $lineno < count($lines); $lineno++) { + $line = $lines[$lineno]; + $kv = explode(':', $line, 2); + if (count($kv) != 2) { + if ($strict) { + return false; + } + continue; + } + + $key = $kv[0]; + $tkey = trim($key); + if ($tkey != $key) { + if ($strict) { + return false; + } + } + + $value = $kv[1]; + $tval = trim($value); + if ($tval != $value) { + if ($strict) { + return false; + } + } + + $values[$tkey] = $tval; + } + + return $values; + } + + /** + * Convert an array into an OpenID colon/newline separated string + * + * @static + * @access private + */ + function fromArray($values) + { + if ($values === null) { + return null; + } + + ksort($values); + + $serialized = ''; + foreach ($values as $key => $value) { + if (is_array($value)) { + list($key, $value) = array($value[0], $value[1]); + } + + if (strpos($key, ':') !== false) { + return null; + } + + if (strpos($key, "\n") !== false) { + return null; + } + + if (strpos($value, "\n") !== false) { + return null; + } + $serialized .= "$key:$value\n"; + } + return $serialized; + } +} + +?>
\ No newline at end of file diff --git a/Auth/OpenID/MemcachedStore.php b/Auth/OpenID/MemcachedStore.php new file mode 100644 index 0000000..d357c6b --- /dev/null +++ b/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/Auth/OpenID/Message.php b/Auth/OpenID/Message.php new file mode 100644 index 0000000..5ab115a --- /dev/null +++ b/Auth/OpenID/Message.php @@ -0,0 +1,920 @@ +<?php + +/** + * Extension argument processing code + * + * @package OpenID + */ + +/** + * Import tools needed to deal with messages. + */ +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', + "http://specs.openid.net/auth/2.0/identifier_select"); + +// URI for Simple Registration extension, the only commonly deployed +// OpenID 1.x extension, and so a special case +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'); + +// The namespace consisting of pairs with keys that are prefixed with +// "openid." but not in another namespace. +define('Auth_OpenID_NULL_NAMESPACE', 'Null namespace'); + +// The null namespace, when it is an allowed OpenID namespace +define('Auth_OpenID_OPENID_NS', 'OpenID namespace'); + +// The top-level namespace, excluding all pairs with keys that start +// with "openid." +define('Auth_OpenID_BARE_NS', 'Bare namespace'); + +// Sentinel for Message implementation to indicate that getArg should +// 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( + 'ns', 'mode', 'error', 'return_to', 'contact', 'reference', + 'signed', 'assoc_type', 'session_type', 'dh_modulus', 'dh_gen', + 'dh_consumer_public', 'claimed_id', 'identity', 'realm', + 'invalidate_handle', 'op_endpoint', 'response_nonce', 'sig', + 'assoc_handle', 'trust_root', 'openid'); + +// Global namespace / alias registration map. See +// Auth_OpenID_registerNamespaceAlias. +global $Auth_OpenID_registered_aliases; +$Auth_OpenID_registered_aliases = array(); + +/** + * Registers a (namespace URI, alias) mapping in a global namespace + * alias map. Raises NamespaceAliasRegistrationError if either the + * namespace URI or alias has already been registered with a different + * value. This function is required if you want to use a namespace + * with an OpenID 1 message. + */ +function Auth_OpenID_registerNamespaceAlias($namespace_uri, $alias) +{ + global $Auth_OpenID_registered_aliases; + + if (Auth_OpenID::arrayGet($Auth_OpenID_registered_aliases, + $alias) == $namespace_uri) { + return true; + } + + if (in_array($namespace_uri, + array_values($Auth_OpenID_registered_aliases))) { + return false; + } + + if (in_array($alias, array_keys($Auth_OpenID_registered_aliases))) { + return false; + } + + $Auth_OpenID_registered_aliases[$alias] = $namespace_uri; + return true; +} + +/** + * Removes a (namespace_uri, alias) registration from the global + * namespace alias map. Returns true if the removal succeeded; false + * if not (if the mapping did not exist). + */ +function Auth_OpenID_removeNamespaceAlias($namespace_uri, $alias) +{ + global $Auth_OpenID_registered_aliases; + + if (Auth_OpenID::arrayGet($Auth_OpenID_registered_aliases, + $alias) === $namespace_uri) { + unset($Auth_OpenID_registered_aliases[$alias]); + return true; + } + + return false; +} + +/** + * An Auth_OpenID_Mapping maintains a mapping from arbitrary keys to + * arbitrary values. (This is unlike an ordinary PHP array, whose + * keys may be only simple scalars.) + * + * @package OpenID + */ +class Auth_OpenID_Mapping { + /** + * Initialize a mapping. If $classic_array is specified, its keys + * and values are used to populate the mapping. + */ + function Auth_OpenID_Mapping($classic_array = null) + { + $this->keys = array(); + $this->values = array(); + + if (is_array($classic_array)) { + foreach ($classic_array as $key => $value) { + $this->set($key, $value); + } + } + } + + /** + * Returns true if $thing is an Auth_OpenID_Mapping object; false + * if not. + */ + function isA($thing) + { + return (is_object($thing) && + strtolower(get_class($thing)) == 'auth_openid_mapping'); + } + + /** + * Returns an array of the keys in the mapping. + */ + function keys() + { + return $this->keys; + } + + /** + * Returns an array of values in the mapping. + */ + function values() + { + return $this->values; + } + + /** + * Returns an array of (key, value) pairs in the mapping. + */ + function items() + { + $temp = array(); + + for ($i = 0; $i < count($this->keys); $i++) { + $temp[] = array($this->keys[$i], + $this->values[$i]); + } + return $temp; + } + + /** + * Returns the "length" of the mapping, or the number of keys. + */ + function len() + { + return count($this->keys); + } + + /** + * Sets a key-value pair in the mapping. If the key already + * exists, its value is replaced with the new value. + */ + function set($key, $value) + { + $index = array_search($key, $this->keys); + + if ($index !== false) { + $this->values[$index] = $value; + } else { + $this->keys[] = $key; + $this->values[] = $value; + } + } + + /** + * Gets a specified value from the mapping, associated with the + * specified key. If the key does not exist in the mapping, + * $default is returned instead. + */ + function get($key, $default = null) + { + $index = array_search($key, $this->keys); + + if ($index !== false) { + return $this->values[$index]; + } else { + return $default; + } + } + + /** + * @access private + */ + function _reflow() + { + // PHP is broken yet again. Sort the arrays to remove the + // hole in the numeric indexes that make up the array. + $old_keys = $this->keys; + $old_values = $this->values; + + $this->keys = array(); + $this->values = array(); + + foreach ($old_keys as $k) { + $this->keys[] = $k; + } + + foreach ($old_values as $v) { + $this->values[] = $v; + } + } + + /** + * Deletes a key-value pair from the mapping with the specified + * key. + */ + function del($key) + { + $index = array_search($key, $this->keys); + + if ($index !== false) { + unset($this->keys[$index]); + unset($this->values[$index]); + $this->_reflow(); + return true; + } + return false; + } + + /** + * Returns true if the specified value has a key in the mapping; + * false if not. + */ + function contains($value) + { + return (array_search($value, $this->keys) !== false); + } +} + +/** + * Maintains a bijective map between namespace uris and aliases. + * + * @package OpenID + */ +class Auth_OpenID_NamespaceMap { + function 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) + { + return $this->namespace_to_alias->get($namespace_uri); + } + + function getNamespaceURI($alias) + { + return $this->alias_to_namespace->get($alias); + } + + function iterNamespaceURIs() + { + // Return an iterator over the namespace URIs + return $this->namespace_to_alias->keys(); + } + + function iterAliases() + { + // Return an iterator over the aliases""" + return $this->alias_to_namespace->keys(); + } + + function iteritems() + { + return $this->namespace_to_alias->items(); + } + + 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; + + // Check that desired_alias is not an openid protocol field as + // per the spec. + if (in_array($desired_alias, $Auth_OpenID_OPENID_PROTOCOL_FIELDS)) { + 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) { + Auth_OpenID::log('"%s" must not contain a dot', $desired_alias); + return null; + } + + // Check that there is not a namespace already defined for the + // desired alias + $current_namespace_uri = + $this->alias_to_namespace->get($desired_alias); + + if (($current_namespace_uri !== null) && + ($current_namespace_uri != $namespace_uri)) { + Auth_OpenID::log('Cannot map "%s" because previous mapping exists', + $namespace_uri); + return null; + } + + // Check that there is not already a (different) alias for + // this namespace URI + $alias = $this->namespace_to_alias->get($namespace_uri); + + if (($alias !== null) && ($alias != $desired_alias)) { + Auth_OpenID::log('Cannot map %s to alias %s. ' . + 'It is already mapped to alias %s', + $namespace_uri, $desired_alias, $alias); + return null; + } + + assert((Auth_OpenID_NULL_NAMESPACE === $desired_alias) || + is_string($desired_alias)); + + $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; + } + + function add($namespace_uri) + { + // Add this namespace URI to the mapping, without caring what + // alias it ends up with + + // See if this namespace is already mapped to an alias + $alias = $this->namespace_to_alias->get($namespace_uri); + + if ($alias !== null) { + return $alias; + } + + // Fall back to generating a numerical alias + $i = 0; + while (1) { + $alias = 'ext' . strval($i); + if ($this->addAlias($namespace_uri, $alias) === null) { + $i += 1; + } else { + return $alias; + } + } + + // Should NEVER be reached! + return null; + } + + function contains($namespace_uri) + { + return $this->isDefined($namespace_uri); + } + + function isDefined($namespace_uri) + { + return $this->namespace_to_alias->contains($namespace_uri); + } +} + +/** + * In the implementation of this object, null represents the global + * namespace as well as a namespace with no key. + * + * @package OpenID + */ +class Auth_OpenID_Message { + + function Auth_OpenID_Message($openid_namespace = null) + { + // 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(); + $this->namespaces = new Auth_OpenID_NamespaceMap(); + if ($openid_namespace === null) { + $this->_openid_ns_uri = null; + } else { + $implicit = Auth_OpenID_isOpenID1($openid_namespace); + $this->setOpenIDNamespace($openid_namespace, $implicit); + } + } + + function isOpenID1() + { + return Auth_OpenID_isOpenID1($this->getOpenIDNamespace()); + } + + function isOpenID2() + { + return $this->getOpenIDNamespace() == Auth_OpenID_OPENID2_NS; + } + + function fromPostArgs($args) + { + // Construct a Message containing a set of POST arguments + $obj = new Auth_OpenID_Message(); + + // Partition into "openid." args and bare args + $openid_args = array(); + foreach ($args as $key => $value) { + + if (is_array($value)) { + return null; + } + + $parts = explode('.', $key, 2); + + if (count($parts) == 2) { + list($prefix, $rest) = $parts; + } else { + $prefix = null; + } + + if ($prefix != 'openid') { + $obj->args->set(array(Auth_OpenID_BARE_NS, $key), $value); + } else { + $openid_args[$rest] = $value; + } + } + + if ($obj->_fromOpenIDArgs($openid_args)) { + return $obj; + } else { + return null; + } + } + + function fromOpenIDArgs($openid_args) + { + // Takes an array. + + // Construct a Message from a parsed KVForm message + $obj = new Auth_OpenID_Message(); + if ($obj->_fromOpenIDArgs($openid_args)) { + return $obj; + } else { + return null; + } + } + + /** + * @access private + */ + function _fromOpenIDArgs($openid_args) + { + global $Auth_OpenID_registered_aliases; + + // Takes an Auth_OpenID_Mapping instance OR an array. + + if (!Auth_OpenID_Mapping::isA($openid_args)) { + $openid_args = new Auth_OpenID_Mapping($openid_args); + } + + $ns_args = array(); + + // Resolve namespaces + foreach ($openid_args->items() as $pair) { + list($rest, $value) = $pair; + + $parts = explode('.', $rest, 2); + + if (count($parts) == 2) { + list($ns_alias, $ns_key) = $parts; + } else { + $ns_alias = Auth_OpenID_NULL_NAMESPACE; + $ns_key = $rest; + } + + if ($ns_alias == 'ns') { + if ($this->namespaces->addAlias($value, $ns_key) === null) { + return false; + } + } else if (($ns_alias == Auth_OpenID_NULL_NAMESPACE) && + ($ns_key == 'ns')) { + // null namespace + if ($this->setOpenIDNamespace($value, false) === false) { + return false; + } + } else { + $ns_args[] = array($ns_alias, $ns_key, $value); + } + } + + if (!$this->getOpenIDNamespace()) { + if ($this->setOpenIDNamespace(Auth_OpenID_OPENID1_NS, true) === + false) { + return false; + } + } + + // 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) { + $ns_uri = $this->_getDefaultNamespace($ns_alias); + if ($ns_uri === null) { + + $ns_uri = Auth_OpenID_OPENID_NS; + $ns_key = sprintf('%s.%s', $ns_alias, $ns_key); + } else { + $this->namespaces->addAlias($ns_uri, $ns_alias, true); + } + } + + $this->setArg($ns_uri, $ns_key, $value); + } + + return true; + } + + 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)) { + 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->_openid_ns_uri = $openid_ns_uri; + + return true; + } + + function getOpenIDNamespace() + { + return $this->_openid_ns_uri; + } + + function fromKVForm($kvform_string) + { + // Create a Message from a KVForm string + return Auth_OpenID_Message::fromOpenIDArgs( + Auth_OpenID_KVForm::toArray($kvform_string)); + } + + function copy() + { + return $this; + } + + function toPostArgs() + { + // Return all arguments with openid. in front of namespaced + // arguments. + + $args = array(); + + // 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) { + $ns_key = 'openid.ns'; + } else { + $ns_key = 'openid.ns.' . $alias; + } + $args[$ns_key] = $ns_uri; + } + + foreach ($this->args->items() as $pair) { + list($ns_parts, $value) = $pair; + list($ns_uri, $ns_key) = $ns_parts; + $key = $this->getKey($ns_uri, $ns_key); + $args[$key] = $value; + } + + return $args; + } + + function toArgs() + { + // Return all namespaced arguments, failing if any + // non-namespaced arguments exist. + $post_args = $this->toPostArgs(); + $kvargs = array(); + foreach ($post_args as $k => $v) { + if (strpos($k, 'openid.') !== 0) { + // raise ValueError( + // 'This message can only be encoded as a POST, because it ' + // 'contains arguments that are not prefixed with "openid."') + return null; + } else { + $kvargs[substr($k, 7)] = $v; + } + } + + return $kvargs; + } + + function toFormMarkup($action_url, $form_tag_attrs = null, + $submit_text = "Continue") + { + $form = "<form accept-charset=\"UTF-8\" ". + "enctype=\"application/x-www-form-urlencoded\""; + + if (!$form_tag_attrs) { + $form_tag_attrs = array(); + } + + $form_tag_attrs['action'] = $action_url; + $form_tag_attrs['method'] = 'post'; + + unset($form_tag_attrs['enctype']); + unset($form_tag_attrs['accept-charset']); + + if ($form_tag_attrs) { + foreach ($form_tag_attrs as $name => $attr) { + $form .= sprintf(" %s=\"%s\"", $name, $attr); + } + } + + $form .= ">\n"; + + foreach ($this->toPostArgs() as $name => $value) { + $form .= sprintf( + "<input type=\"hidden\" name=\"%s\" value=\"%s\" />\n", + $name, $value); + } + + $form .= sprintf("<input type=\"submit\" value=\"%s\" />\n", + $submit_text); + + $form .= "</form>\n"; + + return $form; + } + + function toURL($base_url) + { + // Generate a GET URL with the parameters in this message + // attached as query parameters. + return Auth_OpenID::appendArgs($base_url, $this->toPostArgs()); + } + + function toKVForm() + { + // Generate a KVForm string that contains the parameters in + // this message. This will fail if the message contains + // arguments outside of the 'openid.' prefix. + return Auth_OpenID_KVForm::fromArray($this->toArgs()); + } + + function toURLEncoded() + { + // Generate an x-www-urlencoded string + $args = array(); + + foreach ($this->toPostArgs() as $k => $v) { + $args[] = array($k, $v); + } + + sort($args); + return Auth_OpenID::httpBuildQuery($args); + } + + /** + * @access private + */ + function _fixNS($namespace) + { + // Convert an input value into the internally used values of + // this object + + if ($namespace == Auth_OpenID_OPENID_NS) { + if ($this->_openid_ns_uri === 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))) { + //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) && + (strpos($namespace, ':') === false)) { + // fmt = 'OpenID 2.0 namespace identifiers SHOULD be URIs. Got %r' + // warnings.warn(fmt % (namespace,), DeprecationWarning) + + if ($namespace == 'sreg') { + // fmt = 'Using %r instead of "sreg" as namespace' + // warnings.warn(fmt % (SREG_URI,), DeprecationWarning,) + return Auth_OpenID_SREG_URI; + } + } + + return $namespace; + } + + function hasKey($namespace, $ns_key) + { + $namespace = $this->_fixNS($namespace); + if (Auth_OpenID::isFailure($namespace)) { + // XXX log me + return false; + } else { + return $this->args->contains(array($namespace, $ns_key)); + } + } + + function getKey($namespace, $ns_key) + { + // 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; + } + + $ns_alias = $this->namespaces->getAlias($namespace); + + // No alias is defined, so no key can exist + if ($ns_alias === null) { + return null; + } + + if ($ns_alias == Auth_OpenID_NULL_NAMESPACE) { + $tail = $ns_key; + } else { + $tail = sprintf('%s.%s', $ns_alias, $ns_key); + } + + return 'openid.' . $tail; + } + + function getArg($namespace, $key, $default = null) + { + // Get a value for a namespaced key. + $namespace = $this->_fixNS($namespace); + + if (Auth_OpenID::isFailure($namespace)) { + return $namespace; + } else { + if ((!$this->args->contains(array($namespace, $key))) && + ($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); + } + } + } + + function getArgs($namespace) + { + // Get the arguments that are defined for this namespace URI + + $namespace = $this->_fixNS($namespace); + if (Auth_OpenID::isFailure($namespace)) { + return $namespace; + } else { + $stuff = array(); + foreach ($this->args->items() as $pair) { + list($key, $value) = $pair; + list($pair_ns, $ns_key) = $key; + if ($pair_ns == $namespace) { + $stuff[$ns_key] = $value; + } + } + + return $stuff; + } + } + + function updateArgs($namespace, $updates) + { + // Set multiple key/value pairs in one call + + $namespace = $this->_fixNS($namespace); + + if (Auth_OpenID::isFailure($namespace)) { + return $namespace; + } else { + foreach ($updates as $k => $v) { + $this->setArg($namespace, $k, $v); + } + return true; + } + } + + function setArg($namespace, $key, $value) + { + // Set a single argument in this namespace + $namespace = $this->_fixNS($namespace); + + 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; + } + } + + function delArg($namespace, $key) + { + $namespace = $this->_fixNS($namespace); + + if (Auth_OpenID::isFailure($namespace)) { + return $namespace; + } else { + 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; + + 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) { + $key = $aliased_key; + $ns = $this->getOpenIDNamespace(); + } + + return $this->getArg($ns, $key, $default); + } +} + +?> diff --git a/Auth/OpenID/MySQLStore.php b/Auth/OpenID/MySQLStore.php new file mode 100644 index 0000000..eb08af0 --- /dev/null +++ b/Auth/OpenID/MySQLStore.php @@ -0,0 +1,78 @@ +<?php + +/** + * A MySQL store. + * + * @package OpenID + */ + +/** + * Require the base class file. + */ +require_once "Auth/OpenID/SQLStore.php"; + +/** + * An SQL store that uses MySQL as its backend. + * + * @package OpenID + */ +class Auth_OpenID_MySQLStore extends Auth_OpenID_SQLStore { + /** + * @access private + */ + function setSQL() + { + $this->sql['nonce_table'] = + "CREATE TABLE %s (\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". + ") ENGINE=InnoDB"; + + $this->sql['assoc_table'] = + "CREATE TABLE %s (\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". + ") ENGINE=InnoDB"; + + $this->sql['set_assoc'] = + "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 ". + "WHERE server_url = ?"; + + $this->sql['get_assoc'] = + "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ". + "WHERE server_url = ? AND handle = ?"; + + $this->sql['remove_assoc'] = + "DELETE FROM %s WHERE server_url = ? AND handle = ?"; + + $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 < ?"; + } + + /** + * @access private + */ + function blobEncode($blob) + { + return "0x" . bin2hex($blob); + } +} + +?>
\ No newline at end of file diff --git a/Auth/OpenID/Nonce.php b/Auth/OpenID/Nonce.php new file mode 100644 index 0000000..effecac --- /dev/null +++ b/Auth/OpenID/Nonce.php @@ -0,0 +1,109 @@ +<?php + +/** + * Nonce-related functionality. + * + * @package OpenID + */ + +/** + * Need CryptUtil to generate random strings. + */ +require_once 'Auth/OpenID/CryptUtil.php'; + +/** + * This is the characters that the nonces are made from. + */ +define('Auth_OpenID_Nonce_CHRS',"abcdefghijklmnopqrstuvwxyz" . + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); + +// Keep nonces for five hours (allow five hours for the combination of +// request time and clock skew). This is probably way more than is +// necessary, but there is not much overhead in storing nonces. +global $Auth_OpenID_SKEW; +$Auth_OpenID_SKEW = 60 * 60 * 5; + +define('Auth_OpenID_Nonce_REGEX', + '/(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z(.*)/'); + +define('Auth_OpenID_Nonce_TIME_FMT', + '%Y-%m-%dT%H:%M:%SZ'); + +function Auth_OpenID_splitNonce($nonce_string) +{ + // Extract a timestamp from the given nonce string + $result = preg_match(Auth_OpenID_Nonce_REGEX, $nonce_string, $matches); + if ($result != 1 || count($matches) != 8) { + return null; + } + + list($unused, + $tm_year, + $tm_mon, + $tm_mday, + $tm_hour, + $tm_min, + $tm_sec, + $uniquifier) = $matches; + + $timestamp = + @gmmktime($tm_hour, $tm_min, $tm_sec, $tm_mon, $tm_mday, $tm_year); + + if ($timestamp === false || $timestamp < 0) { + return null; + } + + return array($timestamp, $uniquifier); +} + +function Auth_OpenID_checkTimestamp($nonce_string, + $allowed_skew = null, + $now = null) +{ + // Is the timestamp that is part of the specified nonce string + // within the allowed clock-skew of the current time? + global $Auth_OpenID_SKEW; + + if ($allowed_skew === null) { + $allowed_skew = $Auth_OpenID_SKEW; + } + + $parts = Auth_OpenID_splitNonce($nonce_string); + if ($parts == null) { + return false; + } + + if ($now === null) { + $now = time(); + } + + $stamp = $parts[0]; + + // Time after which we should not use the nonce + $past = $now - $allowed_skew; + + // Time that is too far in the future for us to allow + $future = $now + $allowed_skew; + + // the stamp is not too far in the future and is not too far + // in the past + return (($past <= $stamp) && ($stamp <= $future)); +} + +function Auth_OpenID_mkNonce($when = null) +{ + // Generate a nonce with the current timestamp + $salt = Auth_OpenID_CryptUtil::randomString( + 6, Auth_OpenID_Nonce_CHRS); + if ($when === null) { + // 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; +} + +?>
\ No newline at end of file diff --git a/Auth/OpenID/PAPE.php b/Auth/OpenID/PAPE.php new file mode 100644 index 0000000..62cba8a --- /dev/null +++ b/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/Auth/OpenID/Parse.php b/Auth/OpenID/Parse.php new file mode 100644 index 0000000..546f34f --- /dev/null +++ b/Auth/OpenID/Parse.php @@ -0,0 +1,352 @@ +<?php + +/** + * This module implements a VERY limited parser that finds <link> tags + * in the head of HTML or XHTML documents and parses out their + * attributes according to the OpenID spec. It is a liberal parser, + * but it requires these things from the data in order to work: + * + * - There must be an open <html> tag + * + * - There must be an open <head> tag inside of the <html> tag + * + * - Only <link>s that are found inside of the <head> tag are parsed + * (this is by design) + * + * - The parser follows the OpenID specification in resolving the + * attributes of the link tags. This means that the attributes DO + * NOT get resolved as they would by an XML or HTML parser. In + * particular, only certain entities get replaced, and href + * attributes do not get resolved relative to a base URL. + * + * From http://openid.net/specs.bml: + * + * - The openid.server URL MUST be an absolute URL. OpenID consumers + * MUST NOT attempt to resolve relative URLs. + * + * - The openid.server URL MUST NOT include entities other than &, + * <, >, and ". + * + * The parser ignores SGML comments and <![CDATA[blocks]]>. Both kinds + * of quoting are allowed for attributes. + * + * The parser deals with invalid markup in these ways: + * + * - Tag names are not case-sensitive + * + * - The <html> tag is accepted even when it is not at the top level + * + * - The <head> tag is accepted even when it is not a direct child of + * the <html> tag, but a <html> tag must be an ancestor of the + * <head> tag + * + * - <link> tags are accepted even when they are not direct children + * of the <head> tag, but a <head> tag must be an ancestor of the + * <link> tag + * + * - If there is no closing tag for an open <html> or <head> tag, the + * remainder of the document is viewed as being inside of the + * tag. If there is no closing tag for a <link> tag, the link tag is + * treated as a short tag. Exceptions to this rule are that <html> + * closes <html> and <body> or <head> closes <head> + * + * - Attributes of the <link> tag are not required to be quoted. + * + * - In the case of duplicated attribute names, the attribute coming + * last in the tag will be the value returned. + * + * - Any text that does not parse as an attribute within a link tag + * will be ignored. (e.g. <link pumpkin rel='openid.server' /> will + * ignore pumpkin) + * + * - If there are more than one <html> or <head> tag, the parser only + * looks inside of the first one. + * + * - The contents of <script> tags are ignored entirely, except + * unclosed <script> tags. Unclosed <script> tags are ignored. + * + * - Any other invalid markup is ignored, including unclosed SGML + * comments and unclosed <![CDATA[blocks. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @access private + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Require Auth_OpenID::arrayGet(). + */ +require_once "Auth/OpenID.php"; + +class Auth_OpenID_Parse { + + /** + * Specify some flags for use with regex matching. + */ + var $_re_flags = "si"; + + /** + * Stuff to remove before we start looking for tags + */ + var $_removed_re = + "<!--.*?-->|<!\[CDATA\[.*?\]\]>|<script\b(?!:)[^>]*>.*?<\/script>"; + + /** + * Starts with the tag name at a word boundary, where the tag name + * is not a namespace + */ + var $_tag_expr = "<%s\b(?!:)([^>]*?)(?:\/>|>(.*?)(?:<\/?%s\s*>|\Z))"; + + 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", + $this->_re_flags); + + $this->_entity_replacements = array( + 'amp' => '&', + 'lt' => '<', + 'gt' => '>', + 'quot' => '"' + ); + + $this->_attr_find = sprintf("/%s/%s", + $this->_attr_find, + $this->_re_flags); + + $this->_removed_re = sprintf("/%s/%s", + $this->_removed_re, + $this->_re_flags); + + $this->_ent_replace = + sprintf("&(%s);", implode("|", + $this->_entity_replacements)); + } + + /** + * Returns a regular expression that will match a given tag in an + * SGML string. + */ + 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); + } else { + $closer = $tag_name; + } + + $expr = sprintf($expr, $tag_name, $closer); + return sprintf("/%s/%s", $expr, $this->_re_flags); + } + + function openTag($tag_name) + { + $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', 'html')); + } + + function replaceEntities($str) + { + foreach ($this->_entity_replacements as $old => $new) { + $str = preg_replace(sprintf("/&%s;/", $old), $new, $str); + } + return $str; + } + + function removeQuotes($str) + { + $matches = array(); + $double = '/^"(.*)"$/'; + $single = "/^\'(.*)\'$/"; + + if (preg_match($double, $str, $matches)) { + return $matches[1]; + } else if (preg_match($single, $str, $matches)) { + return $matches[1]; + } else { + return $str; + } + } + + /** + * Find all link tags in a string representing a HTML document and + * return a list of their attributes. + * + * @param string $html The text to parse + * @return array $list An array of arrays of attributes, one for each + * link tag + */ + function parseLinkAttrs($html) + { + $stripped = preg_replace($this->_removed_re, + "", + $html); + + $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, $stripped, $head_matches)) { + return array(); + } + + $link_data = array(); + $link_matches = array(); + + if (!preg_match_all($this->_link_find, $head_matches[0], + $link_matches)) { + return array(); + } + + foreach ($link_matches[0] as $link) { + $attr_matches = array(); + preg_match_all($this->_attr_find, $link, $attr_matches); + $link_attrs = array(); + foreach ($attr_matches[0] as $index => $full_match) { + $name = $attr_matches[1][$index]; + $value = $this->replaceEntities( + $this->removeQuotes($attr_matches[2][$index])); + + $link_attrs[strtolower($name)] = $value; + } + $link_data[] = $link_attrs; + } + + return $link_data; + } + + function relMatches($rel_attr, $target_rel) + { + // Does this target_rel appear in the rel_str? + // XXX: TESTME + $rels = preg_split("/\s+/", trim($rel_attr)); + foreach ($rels as $rel) { + $rel = strtolower($rel); + if ($rel == $target_rel) { + return 1; + } + } + + return 0; + } + + function linkHasRel($link_attrs, $target_rel) + { + // Does this link have target_rel as a relationship? + // XXX: TESTME + $rel_attr = Auth_OpeniD::arrayGet($link_attrs, 'rel', null); + return ($rel_attr && $this->relMatches($rel_attr, + $target_rel)); + } + + function findLinksRel($link_attrs_list, $target_rel) + { + // Filter the list of link attributes on whether it has + // target_rel as a relationship. + // XXX: TESTME + $result = array(); + foreach ($link_attrs_list as $attr) { + if ($this->linkHasRel($attr, $target_rel)) { + $result[] = $attr; + } + } + + return $result; + } + + function findFirstHref($link_attrs_list, $target_rel) + { + // Return the value of the href attribute for the first link + // tag in the list that has target_rel as a relationship. + // XXX: TESTME + $matches = $this->findLinksRel($link_attrs_list, + $target_rel); + if (!$matches) { + return null; + } + $first = $matches[0]; + return Auth_OpenID::arrayGet($first, 'href', null); + } +} + +function Auth_OpenID_legacy_discover($html_text, $server_rel, + $delegate_rel) +{ + $p = new Auth_OpenID_Parse(); + + $link_attrs = $p->parseLinkAttrs($html_text); + + $server_url = $p->findFirstHref($link_attrs, + $server_rel); + + if ($server_url === null) { + return false; + } else { + $delegate_url = $p->findFirstHref($link_attrs, + $delegate_rel); + return array($delegate_url, $server_url); + } +} + +?>
\ No newline at end of file diff --git a/Auth/OpenID/PostgreSQLStore.php b/Auth/OpenID/PostgreSQLStore.php new file mode 100644 index 0000000..69d95e7 --- /dev/null +++ b/Auth/OpenID/PostgreSQLStore.php @@ -0,0 +1,113 @@ +<?php + +/** + * A PostgreSQL store. + * + * @package OpenID + */ + +/** + * Require the base class file. + */ +require_once "Auth/OpenID/SQLStore.php"; + +/** + * An SQL store that uses PostgreSQL as its backend. + * + * @package OpenID + */ +class Auth_OpenID_PostgreSQLStore extends Auth_OpenID_SQLStore { + /** + * @access private + */ + function setSQL() + { + $this->sql['nonce_table'] = + "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) 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))"; + + $this->sql['set_assoc'] = + array( + 'insert_assoc' => "INSERT INTO %s (server_url, handle, ". + "secret, issued, lifetime, assoc_type) VALUES ". + "(?, ?, '!', ?, ?, ?)", + 'update_assoc' => "UPDATE %s SET secret = '!', issued = ?, ". + "lifetime = ?, assoc_type = ? WHERE server_url = ? AND ". + "handle = ?" + ); + + $this->sql['get_assocs'] = + "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ". + "WHERE server_url = ?"; + + $this->sql['get_assoc'] = + "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ". + "WHERE server_url = ? AND handle = ?"; + + $this->sql['remove_assoc'] = + "DELETE FROM %s WHERE server_url = ? AND handle = ?"; + + $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 < ?"; + } + + /** + * @access private + */ + function _set_assoc($server_url, $handle, $secret, $issued, $lifetime, + $assoc_type) + { + $result = $this->_get_assoc($server_url, $handle); + if ($result) { + // Update the table since this associations already exists. + $this->connection->query($this->sql['set_assoc']['update_assoc'], + array($secret, $issued, $lifetime, + $assoc_type, $server_url, $handle)); + } else { + // Insert a new record because this association wasn't + // found. + $this->connection->query($this->sql['set_assoc']['insert_assoc'], + array($server_url, $handle, $secret, + $issued, $lifetime, $assoc_type)); + } + } + + /** + * @access private + */ + function blobEncode($blob) + { + return $this->_octify($blob); + } + + /** + * @access private + */ + function blobDecode($blob) + { + return $this->_unoctify($blob); + } +} + +?>
\ No newline at end of file diff --git a/Auth/OpenID/SQLStore.php b/Auth/OpenID/SQLStore.php new file mode 100644 index 0000000..da93c6a --- /dev/null +++ b/Auth/OpenID/SQLStore.php @@ -0,0 +1,569 @@ +<?php + +/** + * SQL-backed OpenID stores. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Require the PEAR DB module because we'll need it for the SQL-based + * stores implemented here. We silence any errors from the inclusion + * because it might not be present, and a user of the SQL stores may + * supply an Auth_OpenID_DatabaseConnection instance that implements + * its own storage. + */ +global $__Auth_OpenID_PEAR_AVAILABLE; +$__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. + * + * The table names used are determined by the class variables + * associations_table_name and nonces_table_name. To change the name + * of the tables used, pass new table names into the constructor. + * + * To create the tables with the proper schema, see the createTables + * method. + * + * This class shouldn't be used directly. Use one of its subclasses + * instead, as those contain the code necessary to use a specific + * database. If you're an OpenID integrator and you'd like to create + * an SQL-driven store that wraps an application's database + * abstraction, be sure to create a subclass of + * {@link Auth_OpenID_DatabaseConnection} that calls the application's + * database abstraction calls. Then, pass an instance of your new + * database connection class to your SQLStore subclass constructor. + * + * All methods other than the constructor and createTables should be + * considered implementation details. + * + * @package OpenID + */ +class Auth_OpenID_SQLStore extends Auth_OpenID_OpenIDStore { + + /** + * This creates a new SQLStore instance. It requires an + * established database connection be given to it, and it allows + * overriding the default table names. + * + * @param connection $connection This must be an established + * connection to a database of the correct type for the SQLStore + * subclass you're using. This must either be an PEAR DB + * connection handle or an instance of a subclass of + * Auth_OpenID_DatabaseConnection. + * + * @param associations_table: This is an optional parameter to + * specify the name of the table used for storing associations. + * The default value is 'oid_associations'. + * + * @param nonces_table: This is an optional parameter to specify + * the name of the table used for storing nonces. The default + * value is 'oid_nonces'. + */ + function Auth_OpenID_SQLStore($connection, + $associations_table = null, + $nonces_table = null) + { + global $__Auth_OpenID_PEAR_AVAILABLE; + + $this->associations_table_name = "oid_associations"; + $this->nonces_table_name = "oid_nonces"; + + // Check the connection object type to be sure it's a PEAR + // database connection. + if (!(is_object($connection) && + (is_subclass_of($connection, 'db_common') || + is_subclass_of($connection, + 'auth_openid_databaseconnection')))) { + trigger_error("Auth_OpenID_SQLStore expected PEAR connection " . + "object (got ".get_class($connection).")", + E_USER_ERROR); + return; + } + + $this->connection = $connection; + + // Be sure to set the fetch mode so the results are keyed on + // column name instead of column index. This is a PEAR + // constant, so only try to use it if PEAR is present. Note + // that Auth_Openid_Databaseconnection instances need not + // implement ::setFetchMode for this reason. + if ($__Auth_OpenID_PEAR_AVAILABLE) { + $this->connection->setFetchMode(DB_FETCHMODE_ASSOC); + } + + if ($associations_table) { + $this->associations_table_name = $associations_table; + } + + if ($nonces_table) { + $this->nonces_table_name = $nonces_table; + } + + $this->max_nonce_age = 6 * 60 * 60; + + // Be sure to run the database queries with auto-commit mode + // turned OFF, because we want every function to run in a + // transaction, implicitly. As a rule, methods named with a + // leading underscore will NOT control transaction behavior. + // Callers of these methods will worry about transactions. + $this->connection->autoCommit(false); + + // Create an empty SQL strings array. + $this->sql = array(); + + // Call this method (which should be overridden by subclasses) + // to populate the $this->sql array with SQL strings. + $this->setSQL(); + + // Verify that all required SQL statements have been set, and + // raise an error if any expected SQL strings were either + // absent or empty. + list($missing, $empty) = $this->_verifySQL(); + + if ($missing) { + trigger_error("Expected keys in SQL query list: " . + implode(", ", $missing), + E_USER_ERROR); + return; + } + + if ($empty) { + trigger_error("SQL list keys have no SQL strings: " . + implode(", ", $empty), + E_USER_ERROR); + return; + } + + // Add table names to queries. + $this->_fixSQL(); + } + + function tableExists($table_name) + { + return !$this->isError( + $this->connection->query( + sprintf("SELECT * FROM %s LIMIT 0", + $table_name))); + } + + /** + * Returns true if $value constitutes a database error; returns + * false otherwise. + */ + function isError($value) + { + return PEAR::isError($value); + } + + /** + * Converts a query result to a boolean. If the result is a + * database error according to $this->isError(), this returns + * false; otherwise, this returns true. + */ + function resultToBool($obj) + { + if ($this->isError($obj)) { + return false; + } else { + return true; + } + } + + /** + * This method should be overridden by subclasses. This method is + * called by the constructor to set values in $this->sql, which is + * an array keyed on sql name. + */ + function setSQL() + { + } + + /** + * Resets the store by removing all records from the store's + * tables. + */ + function reset() + { + $this->connection->query(sprintf("DELETE FROM %s", + $this->associations_table_name)); + + $this->connection->query(sprintf("DELETE FROM %s", + $this->nonces_table_name)); + } + + /** + * @access private + */ + function _verifySQL() + { + $missing = array(); + $empty = array(); + + $required_sql_keys = array( + 'nonce_table', + 'assoc_table', + 'set_assoc', + 'get_assoc', + 'get_assocs', + 'remove_assoc' + ); + + foreach ($required_sql_keys as $key) { + if (!array_key_exists($key, $this->sql)) { + $missing[] = $key; + } else if (!$this->sql[$key]) { + $empty[] = $key; + } + } + + return array($missing, $empty); + } + + /** + * @access private + */ + function _fixSQL() + { + $replacements = array( + array( + 'value' => $this->nonces_table_name, + 'keys' => array('nonce_table', + 'add_nonce', + 'clean_nonce') + ), + array( + 'value' => $this->associations_table_name, + 'keys' => array('assoc_table', + 'set_assoc', + 'get_assoc', + 'get_assocs', + 'remove_assoc', + 'clean_assoc') + ) + ); + + foreach ($replacements as $item) { + $value = $item['value']; + $keys = $item['keys']; + + foreach ($keys as $k) { + if (is_array($this->sql[$k])) { + foreach ($this->sql[$k] as $part_key => $part_value) { + $this->sql[$k][$part_key] = sprintf($part_value, + $value); + } + } else { + $this->sql[$k] = sprintf($this->sql[$k], $value); + } + } + } + } + + function blobDecode($blob) + { + return $blob; + } + + function blobEncode($str) + { + return $str; + } + + function createTables() + { + $this->connection->autoCommit(true); + $n = $this->create_nonce_table(); + $a = $this->create_assoc_table(); + $this->connection->autoCommit(false); + + if ($n && $a) { + return true; + } else { + return false; + } + } + + function create_nonce_table() + { + if (!$this->tableExists($this->nonces_table_name)) { + $r = $this->connection->query($this->sql['nonce_table']); + return $this->resultToBool($r); + } + return true; + } + + function create_assoc_table() + { + if (!$this->tableExists($this->associations_table_name)) { + $r = $this->connection->query($this->sql['assoc_table']); + return $this->resultToBool($r); + } + return true; + } + + /** + * @access private + */ + function _set_assoc($server_url, $handle, $secret, $issued, + $lifetime, $assoc_type) + { + return $this->connection->query($this->sql['set_assoc'], + array( + $server_url, + $handle, + $secret, + $issued, + $lifetime, + $assoc_type)); + } + + function storeAssociation($server_url, $association) + { + if ($this->resultToBool($this->_set_assoc( + $server_url, + $association->handle, + $this->blobEncode( + $association->secret), + $association->issued, + $association->lifetime, + $association->assoc_type + ))) { + $this->connection->commit(); + } else { + $this->connection->rollback(); + } + } + + /** + * @access private + */ + function _get_assoc($server_url, $handle) + { + $result = $this->connection->getRow($this->sql['get_assoc'], + array($server_url, $handle)); + if ($this->isError($result)) { + return null; + } else { + return $result; + } + } + + /** + * @access private + */ + function _get_assocs($server_url) + { + $result = $this->connection->getAll($this->sql['get_assocs'], + array($server_url)); + + if ($this->isError($result)) { + return array(); + } else { + return $result; + } + } + + function removeAssociation($server_url, $handle) + { + if ($this->_get_assoc($server_url, $handle) == null) { + return false; + } + + if ($this->resultToBool($this->connection->query( + $this->sql['remove_assoc'], + array($server_url, $handle)))) { + $this->connection->commit(); + } else { + $this->connection->rollback(); + } + + return true; + } + + function getAssociation($server_url, $handle = null) + { + if ($handle !== null) { + $assoc = $this->_get_assoc($server_url, $handle); + + $assocs = array(); + if ($assoc) { + $assocs[] = $assoc; + } + } else { + $assocs = $this->_get_assocs($server_url); + } + + if (!$assocs || (count($assocs) == 0)) { + return null; + } else { + $associations = array(); + + foreach ($assocs as $assoc_row) { + $assoc = new Auth_OpenID_Association($assoc_row['handle'], + $assoc_row['secret'], + $assoc_row['issued'], + $assoc_row['lifetime'], + $assoc_row['assoc_type']); + + $assoc->secret = $this->blobDecode($assoc->secret); + + if ($assoc->getExpiresIn() == 0) { + $this->removeAssociation($server_url, $assoc->handle); + } else { + $associations[] = array($assoc->issued, $assoc); + } + } + + if ($associations) { + $issued = array(); + $assocs = array(); + foreach ($associations as $key => $assoc) { + $issued[$key] = $assoc[0]; + $assocs[$key] = $assoc[1]; + } + + array_multisort($issued, SORT_DESC, $assocs, SORT_DESC, + $associations); + + // return the most recently issued one. + list($issued, $assoc) = $associations[0]; + return $assoc; + } else { + return null; + } + } + } + + /** + * @access private + */ + function _add_nonce($server_url, $timestamp, $salt) + { + $sql = $this->sql['add_nonce']; + $result = $this->connection->query($sql, array($server_url, + $timestamp, + $salt)); + if ($this->isError($result)) { + $this->connection->rollback(); + } else { + $this->connection->commit(); + } + return $this->resultToBool($result); + } + + 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); + } + + /** + * "Octifies" a binary string by returning a string with escaped + * octal bytes. This is used for preparing binary data for + * PostgreSQL BYTEA fields. + * + * @access private + */ + function _octify($str) + { + $result = ""; + for ($i = 0; $i < Auth_OpenID::bytes($str); $i++) { + $ch = substr($str, $i, 1); + if ($ch == "\\") { + $result .= "\\\\\\\\"; + } else if (ord($ch) == 0) { + $result .= "\\\\000"; + } else { + $result .= "\\" . strval(decoct(ord($ch))); + } + } + return $result; + } + + /** + * "Unoctifies" octal-escaped data from PostgreSQL and returns the + * resulting ASCII (possibly binary) string. + * + * @access private + */ + function _unoctify($str) + { + $result = ""; + $i = 0; + while ($i < strlen($str)) { + $char = $str[$i]; + if ($char == "\\") { + // Look to see if the next char is a backslash and + // append it. + if ($str[$i + 1] != "\\") { + $octal_digits = substr($str, $i + 1, 3); + $dec = octdec($octal_digits); + $char = chr($dec); + $i += 4; + } else { + $char = "\\"; + $i += 2; + } + } else { + $i += 1; + } + + $result .= $char; + } + + 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/Auth/OpenID/SQLiteStore.php b/Auth/OpenID/SQLiteStore.php new file mode 100644 index 0000000..ec2bf58 --- /dev/null +++ b/Auth/OpenID/SQLiteStore.php @@ -0,0 +1,71 @@ +<?php + +/** + * An SQLite store. + * + * @package OpenID + */ + +/** + * Require the base class file. + */ +require_once "Auth/OpenID/SQLStore.php"; + +/** + * An SQL store that uses SQLite as its backend. + * + * @package OpenID + */ +class Auth_OpenID_SQLiteStore 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))"; + + $this->sql['assoc_table'] = + "CREATE TABLE %s (server_url VARCHAR(2047), handle VARCHAR(255), ". + "secret BLOB(128), issued INTEGER, lifetime INTEGER, ". + "assoc_type VARCHAR(64), PRIMARY KEY (server_url, handle))"; + + $this->sql['set_assoc'] = + "INSERT OR REPLACE INTO %s VALUES (?, ?, ?, ?, ?, ?)"; + + $this->sql['get_assocs'] = + "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ". + "WHERE server_url = ?"; + + $this->sql['get_assoc'] = + "SELECT handle, secret, issued, lifetime, assoc_type FROM %s ". + "WHERE server_url = ? AND handle = ?"; + + $this->sql['remove_assoc'] = + "DELETE FROM %s WHERE server_url = ? AND handle = ?"; + + $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 < ?"; + } + + /** + * @access private + */ + function _add_nonce($server_url, $timestamp, $salt) + { + // PECL SQLite extensions 1.0.3 and older (1.0.3 is the + // current release at the time of this writing) have a broken + // sqlite_escape_string function that breaks when passed the + // empty string. Prefixing all strings with one character + // keeps them unique and avoids this bug. The nonce table is + // write-only, so we don't have to worry about updating other + // functions with this same bad hack. + return parent::_add_nonce('x' . $server_url, $timestamp, $salt); + } +} + +?>
\ No newline at end of file diff --git a/Auth/OpenID/SReg.php b/Auth/OpenID/SReg.php new file mode 100644 index 0000000..6328076 --- /dev/null +++ b/Auth/OpenID/SReg.php @@ -0,0 +1,521 @@ +<?php + +/** + * Simple registration request and response parsing and object + * representation. + * + * This module contains objects representing simple registration + * requests and responses that can be used with both OpenID relying + * parties and OpenID providers. + * + * 1. The relying party creates a request object and adds it to the + * {@link Auth_OpenID_AuthRequest} object before making the + * checkid request to the OpenID provider: + * + * $sreg_req = Auth_OpenID_SRegRequest::build(array('email')); + * $auth_request->addExtension($sreg_req); + * + * 2. The OpenID provider extracts the simple registration request + * from the OpenID request using {@link + * Auth_OpenID_SRegRequest::fromOpenIDRequest}, gets the user's + * approval and data, creates an {@link Auth_OpenID_SRegResponse} + * object and adds it to the id_res response: + * + * $sreg_req = Auth_OpenID_SRegRequest::fromOpenIDRequest( + * $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( + * $sreg_req, $user_data); + * $sreg_resp->toMessage($openid_response->fields); + * + * 3. The relying party uses {@link + * Auth_OpenID_SRegResponse::fromSuccessResponse} to extract the data + * from the OpenID response: + * + * $sreg_resp = Auth_OpenID_SRegResponse::fromSuccessResponse( + * $success_response); + * + * @package OpenID + */ + +/** + * Import message and extension internals. + */ +require_once 'Auth/OpenID/Message.php'; +require_once 'Auth/OpenID/Extension.php'; + +// The data fields that are listed in the sreg spec +global $Auth_OpenID_sreg_data_fields; +$Auth_OpenID_sreg_data_fields = array( + 'fullname' => 'Full Name', + 'nickname' => 'Nickname', + 'dob' => 'Date of Birth', + 'email' => 'E-mail Address', + 'gender' => 'Gender', + 'postcode' => 'Postal Code', + 'country' => 'Country', + 'language' => 'Language', + 'timezone' => 'Time Zone'); + +/** + * Check to see that the given value is a valid simple registration + * data field name. Return true if so, false if not. + */ +function Auth_OpenID_checkFieldName($field_name) +{ + global $Auth_OpenID_sreg_data_fields; + + if (!in_array($field_name, array_keys($Auth_OpenID_sreg_data_fields))) { + return false; + } + return true; +} + +// URI used in the wild for Yadis documents advertising simple +// registration support +define('Auth_OpenID_SREG_NS_URI_1_0', 'http://openid.net/sreg/1.0'); + +// URI in the draft specification for simple registration 1.1 +// <http://openid.net/specs/openid-simple-registration-extension-1_1-01.html> +define('Auth_OpenID_SREG_NS_URI_1_1', 'http://openid.net/extensions/sreg/1.1'); + +// This attribute will always hold the preferred URI to use when +// adding sreg support to an XRDS file or in an OpenID namespace +// declaration. +define('Auth_OpenID_SREG_NS_URI', Auth_OpenID_SREG_NS_URI_1_1); + +Auth_OpenID_registerNamespaceAlias(Auth_OpenID_SREG_NS_URI_1_1, 'sreg'); + +/** + * Does the given endpoint advertise support for simple + * registration? + * + * $endpoint: The endpoint object as returned by OpenID discovery. + * returns whether an sreg type was advertised by the endpoint + */ +function Auth_OpenID_supportsSReg(&$endpoint) +{ + return ($endpoint->usesExtension(Auth_OpenID_SREG_NS_URI_1_1) || + $endpoint->usesExtension(Auth_OpenID_SREG_NS_URI_1_0)); +} + +/** + * A base class for classes dealing with Simple Registration protocol + * messages. + * + * @package OpenID + */ +class Auth_OpenID_SRegBase extends Auth_OpenID_Extension { + /** + * Extract the simple registration namespace URI from the given + * OpenID message. Handles OpenID 1 and 2, as well as both sreg + * namespace URIs found in the wild, as well as missing namespace + * definitions (for OpenID 1) + * + * $message: The OpenID message from which to parse simple + * registration fields. This may be a request or response message. + * + * Returns the sreg namespace URI for the supplied message. The + * message may be modified to define a simple registration + * namespace. + * + * @access private + */ + function _getSRegNS(&$message) + { + $alias = null; + $found_ns_uri = null; + + // See if there exists an alias for one of the two defined + // simple registration types. + foreach (array(Auth_OpenID_SREG_NS_URI_1_1, + Auth_OpenID_SREG_NS_URI_1_0) as $sreg_ns_uri) { + $alias = $message->namespaces->getAlias($sreg_ns_uri); + if ($alias !== null) { + $found_ns_uri = $sreg_ns_uri; + break; + } + } + + if ($alias === null) { + // There is no alias for either of the types, so try to + // add one. We default to using the modern value (1.1) + $found_ns_uri = Auth_OpenID_SREG_NS_URI_1_1; + if ($message->namespaces->addAlias(Auth_OpenID_SREG_NS_URI_1_1, + 'sreg') === null) { + // An alias for the string 'sreg' already exists, but + // it's defined for something other than simple + // registration + return null; + } + } + + return $found_ns_uri; + } +} + +/** + * An object to hold the state of a simple registration request. + * + * required: A list of the required fields in this simple registration + * request + * + * optional: A list of the optional fields in this simple registration + * request + * + * @package OpenID + */ +class Auth_OpenID_SRegRequest extends Auth_OpenID_SRegBase { + + var $ns_alias = 'sreg'; + + /** + * Initialize an empty simple registration request. + */ + function build($required=null, $optional=null, + $policy_url=null, + $sreg_ns_uri=Auth_OpenID_SREG_NS_URI, + $cls='Auth_OpenID_SRegRequest') + { + $obj = new $cls(); + + $obj->required = array(); + $obj->optional = array(); + $obj->policy_url = $policy_url; + $obj->ns_uri = $sreg_ns_uri; + + if ($required) { + if (!$obj->requestFields($required, true, true)) { + return null; + } + } + + if ($optional) { + if (!$obj->requestFields($optional, false, true)) { + return null; + } + } + + return $obj; + } + + /** + * Create a simple registration request that contains the fields + * that were requested in the OpenID request with the given + * arguments + * + * $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($request, $cls='Auth_OpenID_SRegRequest') + { + + $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 = $request->message; + + $obj->ns_uri = $obj->_getSRegNS($m); + $args = $m->getArgs($obj->ns_uri); + + if ($args === null || Auth_OpenID::isFailure($args)) { + return null; + } + + $obj->parseExtensionArgs($args); + + return $obj; + } + + /** + * Parse the unqualified simple registration request parameters + * and add them to this object. + * + * This method is essentially the inverse of + * getExtensionArgs. This method restores the serialized simple + * registration request fields. + * + * If you are extracting arguments from a standard OpenID + * checkid_* request, you probably want to use fromOpenIDRequest, + * which will extract the sreg namespace and arguments from the + * OpenID request. This method is intended for cases where the + * OpenID server needs more control over how the arguments are + * parsed than that method provides. + * + * $args == $message->getArgs($ns_uri); + * $request->parseExtensionArgs($args); + * + * $args: The unqualified simple registration arguments + * + * strict: Whether requests with fields that are not defined in + * the simple registration specification should be tolerated (and + * ignored) + */ + function parseExtensionArgs($args, $strict=false) + { + foreach (array('required', 'optional') as $list_name) { + $required = ($list_name == 'required'); + $items = Auth_OpenID::arrayGet($args, $list_name); + if ($items) { + foreach (explode(',', $items) as $field_name) { + if (!$this->requestField($field_name, $required, $strict)) { + if ($strict) { + return false; + } + } + } + } + } + + $this->policy_url = Auth_OpenID::arrayGet($args, 'policy_url'); + + return true; + } + + /** + * A list of all of the simple registration fields that were + * requested, whether they were required or optional. + */ + function allRequestedFields() + { + return array_merge($this->required, $this->optional); + } + + /** + * Have any simple registration fields been requested? + */ + function wereFieldsRequested() + { + return count($this->allRequestedFields()); + } + + /** + * Was this field in the request? + */ + function contains($field_name) + { + return (in_array($field_name, $this->required) || + in_array($field_name, $this->optional)); + } + + /** + * Request the specified field from the OpenID user + * + * $field_name: the unqualified simple registration field name + * + * required: whether the given field should be presented to the + * user as being a required to successfully complete the request + * + * strict: whether to raise an exception when a field is added to + * a request more than once + */ + function requestField($field_name, + $required=false, $strict=false) + { + if (!Auth_OpenID_checkFieldName($field_name)) { + return false; + } + + if ($strict) { + if ($this->contains($field_name)) { + return false; + } + } else { + if (in_array($field_name, $this->required)) { + return true; + } + + if (in_array($field_name, $this->optional)) { + if ($required) { + unset($this->optional[array_search($field_name, + $this->optional)]); + } else { + return true; + } + } + } + + if ($required) { + $this->required[] = $field_name; + } else { + $this->optional[] = $field_name; + } + + return true; + } + + /** + * Add the given list of fields to the request + * + * field_names: The simple registration data fields to request + * + * required: Whether these values should be presented to the user + * as required + * + * strict: whether to raise an exception when a field is added to + * a request more than once + */ + function requestFields($field_names, $required=false, $strict=false) + { + if (!is_array($field_names)) { + return false; + } + + foreach ($field_names as $field_name) { + if (!$this->requestField($field_name, $required, $strict=$strict)) { + return false; + } + } + + return true; + } + + /** + * Get a dictionary of unqualified simple registration arguments + * representing this request. + * + * This method is essentially the inverse of + * C{L{parseExtensionArgs}}. This method serializes the simple + * registration request fields. + */ + function getExtensionArgs() + { + $args = array(); + + if ($this->required) { + $args['required'] = implode(',', $this->required); + } + + if ($this->optional) { + $args['optional'] = implode(',', $this->optional); + } + + if ($this->policy_url) { + $args['policy_url'] = $this->policy_url; + } + + return $args; + } +} + +/** + * Represents the data returned in a simple registration response + * inside of an OpenID C{id_res} response. This object will be created + * by the OpenID server, added to the C{id_res} response object, and + * then extracted from the C{id_res} message by the Consumer. + * + * @package OpenID + */ +class Auth_OpenID_SRegResponse extends Auth_OpenID_SRegBase { + + var $ns_alias = 'sreg'; + + function Auth_OpenID_SRegResponse($data=null, + $sreg_ns_uri=Auth_OpenID_SREG_NS_URI) + { + if ($data === null) { + $this->data = array(); + } else { + $this->data = $data; + } + + $this->ns_uri = $sreg_ns_uri; + } + + /** + * Take a C{L{SRegRequest}} and a dictionary of simple + * registration values and create a C{L{SRegResponse}} object + * containing that data. + * + * request: The simple registration request object + * + * data: The simple registration data for this response, as a + * dictionary from unqualified simple registration field name to + * string (unicode) value. For instance, the nickname should be + * stored under the key 'nickname'. + */ + function extractResponse($request, $data) + { + $obj = new Auth_OpenID_SRegResponse(); + $obj->ns_uri = $request->ns_uri; + + foreach ($request->allRequestedFields() as $field) { + $value = Auth_OpenID::arrayGet($data, $field); + if ($value !== null) { + $obj->data[$field] = $value; + } + } + + return $obj; + } + + /** + * Create a C{L{SRegResponse}} object from a successful OpenID + * library response + * (C{L{openid.consumer.consumer.SuccessResponse}}) response + * message + * + * success_response: A SuccessResponse from consumer.complete() + * + * signed_only: Whether to process only data that was + * signed in the id_res message from the server. + * + * Returns a simple registration response containing the data that + * was supplied with the C{id_res} response. + */ + function fromSuccessResponse(&$success_response, $signed_only=true) + { + global $Auth_OpenID_sreg_data_fields; + + $obj = new Auth_OpenID_SRegResponse(); + $obj->ns_uri = $obj->_getSRegNS($success_response->message); + + if ($signed_only) { + $args = $success_response->getSignedNS($obj->ns_uri); + } else { + $args = $success_response->message->getArgs($obj->ns_uri); + } + + if ($args === null || Auth_OpenID::isFailure($args)) { + return null; + } + + foreach ($Auth_OpenID_sreg_data_fields as $field_name => $desc) { + if (in_array($field_name, array_keys($args))) { + $obj->data[$field_name] = $args[$field_name]; + } + } + + return $obj; + } + + function getExtensionArgs() + { + return $this->data; + } + + // Read-only dictionary interface + function get($field_name, $default=null) + { + if (!Auth_OpenID_checkFieldName($field_name)) { + return null; + } + + return Auth_OpenID::arrayGet($this->data, $field_name, $default); + } + + function contents() + { + return $this->data; + } +} + +?> diff --git a/Auth/OpenID/Server.php b/Auth/OpenID/Server.php new file mode 100644 index 0000000..f1db4d8 --- /dev/null +++ b/Auth/OpenID/Server.php @@ -0,0 +1,1760 @@ +<?php + +/** + * OpenID server protocol and logic. + * + * Overview + * + * An OpenID server must perform three tasks: + * + * 1. Examine the incoming request to determine its nature and validity. + * 2. Make a decision about how to respond to this request. + * 3. Format the response according to the protocol. + * + * The first and last of these tasks may performed by the {@link + * Auth_OpenID_Server::decodeRequest()} and {@link + * Auth_OpenID_Server::encodeResponse} methods. Who gets to do the + * intermediate task -- deciding how to respond to the request -- will + * depend on what type of request it is. + * + * If it's a request to authenticate a user (a 'checkid_setup' or + * 'checkid_immediate' request), you need to decide if you will assert + * that this user may claim the identity in question. Exactly how you + * do that is a matter of application policy, but it generally + * involves making sure the user has an account with your system and + * is logged in, checking to see if that identity is hers to claim, + * and verifying with the user that she does consent to releasing that + * information to the party making the request. + * + * Examine the properties of the {@link Auth_OpenID_CheckIDRequest} + * object, and if and when you've come to a decision, form a response + * by calling {@link Auth_OpenID_CheckIDRequest::answer()}. + * + * Other types of requests relate to establishing associations between + * client and server and verifing the authenticity of previous + * communications. {@link Auth_OpenID_Server} contains all the logic + * and data necessary to respond to such requests; just pass it to + * {@link Auth_OpenID_Server::handleRequest()}. + * + * OpenID Extensions + * + * Do you want to provide other information for your users in addition + * to authentication? Version 1.2 of the OpenID protocol allows + * consumers to add extensions to their requests. For example, with + * sites using the Simple Registration + * Extension + * (http://www.openidenabled.com/openid/simple-registration-extension/), + * a user can agree to have their nickname and e-mail address sent to + * a site when they sign up. + * + * Since extensions do not change the way OpenID authentication works, + * code to handle extension requests may be completely separate from + * the {@link Auth_OpenID_Request} class here. But you'll likely want + * data sent back by your extension to be signed. {@link + * Auth_OpenID_ServerResponse} provides methods with which you can add + * data to it which can be signed with the other data in the OpenID + * signature. + * + * For example: + * + * <pre> // when request is a checkid_* request + * $response = $request->answer(true); + * // this will a signed 'openid.sreg.timezone' parameter to the response + * response.addField('sreg', 'timezone', 'America/Los_Angeles')</pre> + * + * Stores + * + * The OpenID server needs to maintain state between requests in order + * to function. Its mechanism for doing this is called a store. The + * store interface is defined in Interface.php. Additionally, several + * concrete store implementations are provided, so that most sites + * won't need to implement a custom store. For a store backed by flat + * files on disk, see {@link Auth_OpenID_FileStore}. For stores based + * on MySQL, SQLite, or PostgreSQL, see the {@link + * Auth_OpenID_SQLStore} subclasses. + * + * Upgrading + * + * The keys by which a server looks up associations in its store have + * changed in version 1.2 of this library. If your store has entries + * created from version 1.0 code, you should empty it. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Required imports + */ +require_once "Auth/OpenID.php"; +require_once "Auth/OpenID/Association.php"; +require_once "Auth/OpenID/CryptUtil.php"; +require_once "Auth/OpenID/BigMath.php"; +require_once "Auth/OpenID/DiffieHellman.php"; +require_once "Auth/OpenID/KVForm.php"; +require_once "Auth/OpenID/TrustRoot.php"; +require_once "Auth/OpenID/ServerRequest.php"; +require_once "Auth/OpenID/Message.php"; +require_once "Auth/OpenID/Nonce.php"; + +define('AUTH_OPENID_HTTP_OK', 200); +define('AUTH_OPENID_HTTP_REDIRECT', 302); +define('AUTH_OPENID_HTTP_ERROR', 400); + +/** + * @access private + */ +global $_Auth_OpenID_Request_Modes; +$_Auth_OpenID_Request_Modes = array('checkid_setup', + 'checkid_immediate'); + +/** + * @access private + */ +define('Auth_OpenID_ENCODE_KVFORM', 'kfvorm'); + +/** + * @access private + */ +define('Auth_OpenID_ENCODE_URL', 'URL/redirect'); + +/** + * @access private + */ +define('Auth_OpenID_ENCODE_HTML_FORM', 'HTML form'); + +/** + * @access private + */ +function Auth_OpenID_isError($obj, $cls = 'Auth_OpenID_ServerError') +{ + return is_a($obj, $cls); +} + +/** + * An error class which gets instantiated and returned whenever an + * OpenID protocol error occurs. Be prepared to use this in place of + * an ordinary server response. + * + * @package OpenID + */ +class Auth_OpenID_ServerError { + /** + * @access private + */ + function Auth_OpenID_ServerError($message = null, $text = null, + $reference = null, $contact = null) + { + $this->message = $message; + $this->text = $text; + $this->contact = $contact; + $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() + { + return $this->getReturnTo() !== null; + } + + /** + * Encodes this error's response as a URL suitable for + * redirection. If the response has no return_to, another + * Auth_OpenID_ServerError is returned. + */ + function encodeToURL() + { + if (!$this->message) { + return null; + } + + $msg = $this->toMessage(); + return $msg->toURL($this->getReturnTo()); + } + + /** + * Encodes the response to key-value form. This is a + * machine-readable format used to respond to messages which came + * directly from the consumer and not through the user-agent. See + * the OpenID specification. + */ + function encodeToKVForm() + { + return Auth_OpenID_KVForm::fromArray( + array('mode' => 'error', + '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, + // after encoding. + $namespace = $this->message->getOpenIDNamespace(); + $reply = new Auth_OpenID_Message($namespace); + $reply->setArg(Auth_OpenID_OPENID_NS, 'mode', 'error'); + $reply->setArg(Auth_OpenID_OPENID_NS, 'error', $this->toString()); + + if ($this->contact !== null) { + $reply->setArg(Auth_OpenID_OPENID_NS, 'contact', $this->contact); + } + + if ($this->reference !== null) { + $reply->setArg(Auth_OpenID_OPENID_NS, 'reference', + $this->reference); + } + + return $reply; + } + + /** + * 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_Request_Modes; + + if ($this->hasReturnTo()) { + 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) { + return null; + } + + $mode = $this->message->getArg(Auth_OpenID_OPENID_NS, + 'mode'); + + if ($mode) { + if (!in_array($mode, $_Auth_OpenID_Request_Modes)) { + return Auth_OpenID_ENCODE_KVFORM; + } + } + return null; + } + + /** + * Returns this error message. + */ + function toString() + { + if ($this->text) { + return $this->text; + } else { + return get_class($this) . " error"; + } + } +} + +/** + * Error returned by the server code when a return_to is absent from a + * request. + * + * @package OpenID + */ +class Auth_OpenID_NoReturnToError extends Auth_OpenID_ServerError { + function Auth_OpenID_NoReturnToError($message = null, + $text = "No return_to URL available") + { + parent::Auth_OpenID_ServerError($message, $text); + } + + function toString() + { + return "No return_to available"; + } +} + +/** + * An error indicating that the return_to URL is malformed. + * + * @package OpenID + */ +class Auth_OpenID_MalformedReturnURL extends Auth_OpenID_ServerError { + function Auth_OpenID_MalformedReturnURL($message, $return_to) + { + $this->return_to = $return_to; + parent::Auth_OpenID_ServerError($message, "malformed return_to URL"); + } +} + +/** + * This error is returned when the trust_root value is malformed. + * + * @package OpenID + */ +class Auth_OpenID_MalformedTrustRoot extends Auth_OpenID_ServerError { + function Auth_OpenID_MalformedTrustRoot($message = null, + $text = "Malformed trust root") + { + parent::Auth_OpenID_ServerError($message, $text); + } + + function toString() + { + return "Malformed trust root"; + } +} + +/** + * The base class for all server request classes. + * + * @package OpenID + */ +class Auth_OpenID_Request { + var $mode = null; +} + +/** + * A request to verify the validity of a previous response. + * + * @package OpenID + */ +class Auth_OpenID_CheckAuthRequest extends Auth_OpenID_Request { + var $mode = "check_authentication"; + var $invalidate_handle = null; + + function Auth_OpenID_CheckAuthRequest($assoc_handle, $signed, + $invalidate_handle = null) + { + $this->assoc_handle = $assoc_handle; + $this->signed = $signed; + if ($invalidate_handle !== null) { + $this->invalidate_handle = $invalidate_handle; + } + $this->namespace = Auth_OpenID_OPENID2_NS; + $this->message = null; + } + + function fromMessage($message, $server=null) + { + $required_keys = array('assoc_handle', 'sig', 'signed'); + + foreach ($required_keys as $k) { + if (!$message->getArg(Auth_OpenID_OPENID_NS, $k)) { + return new Auth_OpenID_ServerError($message, + sprintf("%s request missing required parameter %s from \ + query", "check_authentication", $k)); + } + } + + $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS, 'assoc_handle'); + $sig = $message->getArg(Auth_OpenID_OPENID_NS, 'sig'); + + $signed_list = $message->getArg(Auth_OpenID_OPENID_NS, 'signed'); + $signed_list = explode(",", $signed_list); + + $signed = $message; + if ($signed->hasKey(Auth_OpenID_OPENID_NS, 'mode')) { + $signed->setArg(Auth_OpenID_OPENID_NS, 'mode', 'id_res'); + } + + $result = new Auth_OpenID_CheckAuthRequest($assoc_handle, $signed); + $result->message = $message; + $result->sig = $sig; + $result->invalidate_handle = $message->getArg(Auth_OpenID_OPENID_NS, + 'invalidate_handle'); + return $result; + } + + function answer(&$signatory) + { + $is_valid = $signatory->verify($this->assoc_handle, $this->signed); + + // Now invalidate that assoc_handle so it this checkAuth + // message cannot be replayed. + $signatory->invalidate($this->assoc_handle, true); + $response = new Auth_OpenID_ServerResponse($this); + + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'is_valid', + ($is_valid ? "true" : "false")); + + if ($this->invalidate_handle) { + $assoc = $signatory->getAssociation($this->invalidate_handle, + false); + if (!$assoc) { + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'invalidate_handle', + $this->invalidate_handle); + } + } + return $response; + } +} + +/** + * A class implementing plaintext server sessions. + * + * @package OpenID + */ +class Auth_OpenID_PlainTextServerSession { + /** + * An object that knows how to handle association requests with no + * session type. + */ + var $session_type = 'no-encryption'; + var $needs_math = false; + var $allowed_assoc_types = array('HMAC-SHA1', 'HMAC-SHA256'); + + function fromMessage($unused_request) + { + return new Auth_OpenID_PlainTextServerSession(); + } + + function answer($secret) + { + return array('mac_key' => base64_encode($secret)); + } +} + +/** + * A class implementing DH-SHA1 server sessions. + * + * @package OpenID + */ +class Auth_OpenID_DiffieHellmanSHA1ServerSession { + /** + * An object that knows how to handle association requests with + * the Diffie-Hellman session type. + */ + + var $session_type = 'DH-SHA1'; + var $needs_math = true; + var $allowed_assoc_types = array('HMAC-SHA1'); + var $hash_func = 'Auth_OpenID_SHA1'; + + function Auth_OpenID_DiffieHellmanSHA1ServerSession($dh, $consumer_pubkey) + { + $this->dh = $dh; + $this->consumer_pubkey = $consumer_pubkey; + } + + function getDH($message) + { + $dh_modulus = $message->getArg(Auth_OpenID_OPENID_NS, 'dh_modulus'); + $dh_gen = $message->getArg(Auth_OpenID_OPENID_NS, 'dh_gen'); + + if ((($dh_modulus === null) && ($dh_gen !== null)) || + (($dh_gen === null) && ($dh_modulus !== null))) { + + if ($dh_modulus === null) { + $missing = 'modulus'; + } else { + $missing = 'generator'; + } + + return new Auth_OpenID_ServerError($message, + 'If non-default modulus or generator is '. + 'supplied, both must be supplied. Missing '. + $missing); + } + + $lib =& Auth_OpenID_getMathLib(); + + if ($dh_modulus || $dh_gen) { + $dh_modulus = $lib->base64ToLong($dh_modulus); + $dh_gen = $lib->base64ToLong($dh_gen); + if ($lib->cmp($dh_modulus, 0) == 0 || + $lib->cmp($dh_gen, 0) == 0) { + return new Auth_OpenID_ServerError( + $message, "Failed to parse dh_mod or dh_gen"); + } + $dh = new Auth_OpenID_DiffieHellman($dh_modulus, $dh_gen); + } else { + $dh = new Auth_OpenID_DiffieHellman(); + } + + $consumer_pubkey = $message->getArg(Auth_OpenID_OPENID_NS, + 'dh_consumer_public'); + if ($consumer_pubkey === null) { + return new Auth_OpenID_ServerError($message, + 'Public key for DH-SHA1 session '. + 'not found in query'); + } + + $consumer_pubkey = + $lib->base64ToLong($consumer_pubkey); + + if ($consumer_pubkey === false) { + return new Auth_OpenID_ServerError($message, + "dh_consumer_public is not base64"); + } + + return array($dh, $consumer_pubkey); + } + + function fromMessage($message) + { + $result = Auth_OpenID_DiffieHellmanSHA1ServerSession::getDH($message); + + if (is_a($result, 'Auth_OpenID_ServerError')) { + return $result; + } else { + list($dh, $consumer_pubkey) = $result; + return new Auth_OpenID_DiffieHellmanSHA1ServerSession($dh, + $consumer_pubkey); + } + } + + function answer($secret) + { + $lib =& Auth_OpenID_getMathLib(); + $mac_key = $this->dh->xorSecret($this->consumer_pubkey, $secret, + $this->hash_func); + return array( + 'dh_server_public' => + $lib->longToBase64($this->dh->public), + 'enc_mac_key' => base64_encode($mac_key)); + } +} + +/** + * A class implementing DH-SHA256 server sessions. + * + * @package OpenID + */ +class Auth_OpenID_DiffieHellmanSHA256ServerSession + extends Auth_OpenID_DiffieHellmanSHA1ServerSession { + + var $session_type = 'DH-SHA256'; + var $hash_func = 'Auth_OpenID_SHA256'; + var $allowed_assoc_types = array('HMAC-SHA256'); + + function fromMessage($message) + { + $result = Auth_OpenID_DiffieHellmanSHA1ServerSession::getDH($message); + + if (is_a($result, 'Auth_OpenID_ServerError')) { + return $result; + } else { + list($dh, $consumer_pubkey) = $result; + return new Auth_OpenID_DiffieHellmanSHA256ServerSession($dh, + $consumer_pubkey); + } + } +} + +/** + * A request to associate with the server. + * + * @package OpenID + */ +class Auth_OpenID_AssociateRequest extends Auth_OpenID_Request { + var $mode = "associate"; + + function getSessionClasses() + { + return array( + 'no-encryption' => 'Auth_OpenID_PlainTextServerSession', + 'DH-SHA1' => 'Auth_OpenID_DiffieHellmanSHA1ServerSession', + 'DH-SHA256' => 'Auth_OpenID_DiffieHellmanSHA256ServerSession'); + } + + function Auth_OpenID_AssociateRequest(&$session, $assoc_type) + { + $this->session =& $session; + $this->namespace = Auth_OpenID_OPENID2_NS; + $this->assoc_type = $assoc_type; + } + + function fromMessage($message, $server=null) + { + if ($message->isOpenID1()) { + $session_type = $message->getArg(Auth_OpenID_OPENID_NS, + 'session_type'); + + if ($session_type == 'no-encryption') { + // oidutil.log('Received OpenID 1 request with a no-encryption ' + // 'assocaition session type. Continuing anyway.') + } else if (!$session_type) { + $session_type = 'no-encryption'; + } + } else { + $session_type = $message->getArg(Auth_OpenID_OPENID_NS, + 'session_type'); + if ($session_type === null) { + return new Auth_OpenID_ServerError($message, + "session_type missing from request"); + } + } + + $session_class = Auth_OpenID::arrayGet( + Auth_OpenID_AssociateRequest::getSessionClasses(), + $session_type); + + if ($session_class === null) { + return new Auth_OpenID_ServerError($message, + "Unknown session type " . + $session_type); + } + + $session = call_user_func(array($session_class, 'fromMessage'), + $message); + if (is_a($session, 'Auth_OpenID_ServerError')) { + return $session; + } + + $assoc_type = $message->getArg(Auth_OpenID_OPENID_NS, + 'assoc_type', 'HMAC-SHA1'); + + if (!in_array($assoc_type, $session->allowed_assoc_types)) { + $fmt = "Session type %s does not support association type %s"; + return new Auth_OpenID_ServerError($message, + sprintf($fmt, $session_type, $assoc_type)); + } + + $obj = new Auth_OpenID_AssociateRequest($session, $assoc_type); + $obj->message = $message; + $obj->namespace = $message->getOpenIDNamespace(); + return $obj; + } + + function answer($assoc) + { + $response = new Auth_OpenID_ServerResponse($this); + $response->fields->updateArgs(Auth_OpenID_OPENID_NS, + array( + 'expires_in' => sprintf('%d', $assoc->getExpiresIn()), + 'assoc_type' => $this->assoc_type, + 'assoc_handle' => $assoc->handle)); + + $response->fields->updateArgs(Auth_OpenID_OPENID_NS, + $this->session->answer($assoc->secret)); + + if (! ($this->session->session_type == 'no-encryption' + && $this->message->isOpenID1())) { + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'session_type', + $this->session->session_type); + } + + return $response; + } + + function answerUnsupported($text_message, + $preferred_association_type=null, + $preferred_session_type=null) + { + if ($this->message->isOpenID1()) { + return new Auth_OpenID_ServerError($this->message); + } + + $response = new Auth_OpenID_ServerResponse($this); + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'error_code', 'unsupported-type'); + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'error', $text_message); + + if ($preferred_association_type) { + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'assoc_type', + $preferred_association_type); + } + + if ($preferred_session_type) { + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'session_type', + $preferred_session_type); + } + + return $response; + } +} + +/** + * A request to confirm the identity of a user. + * + * @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. + */ + var $mode = "checkid_setup"; // or "checkid_immediate" + + /** + * Whether this request is for immediate mode. + */ + var $immediate = false; + + /** + * The trust_root value for this 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) + { + if ($server === null) { + return new Auth_OpenID_ServerError($message, + "server must not be null"); + } + + if ($return_to && + !Auth_OpenID_TrustRoot::_parse($return_to)) { + return new Auth_OpenID_MalformedReturnURL($message, $return_to); + } + + $r = new Auth_OpenID_CheckIDRequest($identity, $return_to, + $trust_root, $immediate, + $assoc_handle, $server); + + $r->namespace = $message->getOpenIDNamespace(); + $r->message =& $message; + + if (!$r->trustRootValid()) { + return new Auth_OpenID_UntrustedReturnURL($message, + $return_to, + $trust_root); + } else { + return $r; + } + } + + function Auth_OpenID_CheckIDRequest($identity, $return_to, + $trust_root = null, $immediate = false, + $assoc_handle = null, $server = null, + $claimed_id = null) + { + $this->namespace = Auth_OpenID_OPENID2_NS; + $this->assoc_handle = $assoc_handle; + $this->identity = $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; + + if ($immediate) { + $this->immediate = true; + $this->mode = "checkid_immediate"; + } else { + $this->immediate = false; + $this->mode = "checkid_setup"; + } + } + + function equals($other) + { + return ( + (is_a($other, 'Auth_OpenID_CheckIDRequest')) && + ($this->namespace == $other->namespace) && + ($this->assoc_handle == $other->assoc_handle) && + ($this->identity == $other->identity) && + ($this->claimed_id == $other->claimed_id) && + ($this->return_to == $other->return_to) && + ($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'); + $immediate = null; + + if ($mode == "checkid_immediate") { + $immediate = true; + $mode = "checkid_immediate"; + } else { + $immediate = false; + $mode = "checkid_setup"; + } + + $return_to = $message->getArg(Auth_OpenID_OPENID_NS, + 'return_to'); + + if (($message->isOpenID1()) && + (!$return_to)) { + $fmt = "Missing required field 'return_to' from checkid request"; + return new Auth_OpenID_ServerError($message, $fmt); + } + + $identity = $message->getArg(Auth_OpenID_OPENID_NS, + 'identity'); + $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 { + 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 ($message->isOpenID1()) { + $trust_root_param = 'trust_root'; + } else { + $trust_root_param = 'realm'; + } + $trust_root = $message->getArg(Auth_OpenID_OPENID_NS, + $trust_root_param); + if (! $trust_root) { + $trust_root = $return_to; + } + + 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, + 'assoc_handle'); + + $obj = Auth_OpenID_CheckIDRequest::make($message, + $identity, + $return_to, + $trust_root, + $immediate, + $assoc_handle, + $server); + + if (is_a($obj, 'Auth_OpenID_ServerError')) { + return $obj; + } + + $obj->claimed_id = $claimed_id; + + return $obj; + } + + function idSelect() + { + // Is the identifier to be selected by the IDP? + // So IDPs don't have to import the constant + return $this->identity == Auth_OpenID_IDENTIFIER_SELECT; + } + + function trustRootValid() + { + if (!$this->trust_root) { + return true; + } + + $tr = Auth_OpenID_TrustRoot::_parse($this->trust_root); + if ($tr === false) { + return new Auth_OpenID_MalformedTrustRoot($this->message, + $this->trust_root); + } + + if ($this->return_to !== null) { + return Auth_OpenID_TrustRoot::match($this->trust_root, + $this->return_to); + } else { + return true; + } + } + + /** + * Respond to this request. Return either an + * {@link Auth_OpenID_ServerResponse} or + * {@link Auth_OpenID_ServerError}. + * + * @param bool $allow Allow this user to claim this identity, and + * allow the consumer to have this information? + * + * @param string $server_url DEPRECATED. Passing $op_endpoint to + * the {@link Auth_OpenID_Server} constructor makes this optional. + * + * When an OpenID 1.x immediate mode request does not succeed, it + * gets back a URL where the request may be carried out in a + * not-so-immediate fashion. Pass my URL in here (the fully + * qualified address of this server's endpoint, i.e. + * http://example.com/server), and I will use it as a base for the + * URL for a new request. + * + * Optional for requests where {@link $immediate} is false or + * $allow is true. + * + * @param string $identity The OP-local identifier to answer with. + * Only for use when the relying party requested identifier + * selection. + * + * @param string $claimed_id The claimed identifier to answer + * with, for use with identifier selection in the case where the + * claimed identifier and the OP-local identifier differ, + * i.e. when the claimed_id uses delegation. + * + * If $identity is provided but this is not, $claimed_id will + * default to the value of $identity. When answering requests + * that did not ask for identifier selection, the response + * $claimed_id will default to that of the request. + * + * This parameter is new in OpenID 2.0. + * + * @return mixed + */ + function answer($allow, $server_url = null, $identity = null, + $claimed_id = null) + { + if (!$this->return_to) { + return new Auth_OpenID_NoReturnToError(); + } + + if (!$server_url) { + if ((!$this->message->isOpenID1()) && + (!$this->server->op_endpoint)) { + return new Auth_OpenID_ServerError(null, + "server should be constructed with op_endpoint to " . + "respond to OpenID 2.0 messages."); + } + + $server_url = $this->server->op_endpoint; + } + + if ($allow) { + $mode = 'id_res'; + } else if ($this->message->isOpenID1()) { + if ($this->immediate) { + $mode = 'id_res'; + } else { + $mode = 'cancel'; + } + } else { + if ($this->immediate) { + $mode = 'setup_needed'; + } else { + $mode = 'cancel'; + } + } + + if (!$this->trustRootValid()) { + return new Auth_OpenID_UntrustedReturnURL(null, + $this->return_to, + $this->trust_root); + } + + $response = new Auth_OpenID_ServerResponse($this); + + if ($claimed_id && + ($this->message->isOpenID1())) { + return new Auth_OpenID_ServerError(null, + "claimed_id is new in OpenID 2.0 and not " . + "available for ".$this->namespace); + } + + if ($identity && !$claimed_id) { + $claimed_id = $identity; + } + + if ($allow) { + + if ($this->identity == Auth_OpenID_IDENTIFIER_SELECT) { + if (!$identity) { + return new Auth_OpenID_ServerError(null, + "This request uses IdP-driven identifier selection. " . + "You must supply an identifier in the response."); + } + + $response_identity = $identity; + $response_claimed_id = $claimed_id; + + } else if ($this->identity) { + if ($identity && + ($this->identity != $identity)) { + $fmt = "Request was for %s, cannot reply with identity %s"; + return new Auth_OpenID_ServerError(null, + sprintf($fmt, $this->identity, $identity)); + } + + $response_identity = $this->identity; + $response_claimed_id = $this->claimed_id; + } else { + if ($identity) { + return new Auth_OpenID_ServerError(null, + "This request specified no identity and " . + "you supplied ".$identity); + } + + $response_identity = null; + } + + if (($this->message->isOpenID1()) && + ($response_identity === null)) { + return new Auth_OpenID_ServerError(null, + "Request was an OpenID 1 request, so response must " . + "include an identifier."); + } + + $response->fields->updateArgs(Auth_OpenID_OPENID_NS, + array('mode' => $mode, + '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->message->isOpenID2()) { + $response->fields->setArg( + Auth_OpenID_OPENID_NS, + 'claimed_id', + $response_claimed_id); + } + } + + } else { + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'mode', $mode); + + if ($this->immediate) { + if (($this->message->isOpenID1()) && + (!$server_url)) { + return new Auth_OpenID_ServerError(null, + 'setup_url is required for $allow=false \ + in OpenID 1.x immediate mode.'); + } + + $setup_request =& new Auth_OpenID_CheckIDRequest( + $this->identity, + $this->return_to, + $this->trust_root, + false, + $this->assoc_handle, + $this->server, + $this->claimed_id); + $setup_request->message = $this->message; + + $setup_url = $setup_request->encodeToURL($server_url); + + if ($setup_url === null) { + return new Auth_OpenID_NoReturnToError(); + } + + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'user_setup_url', + $setup_url); + } + } + + return $response; + } + + function encodeToURL($server_url) + { + if (!$this->return_to) { + return new Auth_OpenID_NoReturnToError(); + } + + // Imported from the alternate reality where these classes are + // used in both the client and server code, so Requests are + // Encodable too. That's right, code imported from alternate + // realities all for the love of you, id_res/user_setup_url. + + $q = array('mode' => $this->mode, + 'identity' => $this->identity, + 'claimed_id' => $this->claimed_id, + 'return_to' => $this->return_to); + + if ($this->trust_root) { + if ($this->message->isOpenID1()) { + $q['trust_root'] = $this->trust_root; + } else { + $q['realm'] = $this->trust_root; + } + } + + if ($this->assoc_handle) { + $q['assoc_handle'] = $this->assoc_handle; + } + + $response = new Auth_OpenID_Message( + $this->message->getOpenIDNamespace()); + $response->updateArgs(Auth_OpenID_OPENID_NS, $q); + return $response->toURL($server_url); + } + + function getCancelURL() + { + if (!$this->return_to) { + return new Auth_OpenID_NoReturnToError(); + } + + if ($this->immediate) { + return new Auth_OpenID_ServerError(null, + "Cancel is not an appropriate \ + response to immediate mode \ + requests."); + } + + $response = new Auth_OpenID_Message( + $this->message->getOpenIDNamespace()); + $response->setArg(Auth_OpenID_OPENID_NS, 'mode', 'cancel'); + return $response->toURL($this->return_to); + } +} + +/** + * This class encapsulates the response to an OpenID server request. + * + * @package OpenID + */ +class Auth_OpenID_ServerResponse { + + function Auth_OpenID_ServerResponse(&$request) + { + $this->request =& $request; + $this->fields = new Auth_OpenID_Message($this->request->namespace); + } + + function whichEncoding() + { + global $_Auth_OpenID_Request_Modes; + + if (in_array($this->request->mode, $_Auth_OpenID_Request_Modes)) { + 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; + } + } + + /* + * 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, + 'mode') == 'id_res'; + } + + function encodeToKVForm() + { + return $this->fields->toKVForm(); + } +} + +/** + * A web-capable response object which you can use to generate a + * user-agent response. + * + * @package OpenID + */ +class Auth_OpenID_WebResponse { + var $code = AUTH_OPENID_HTTP_OK; + var $body = ""; + + function Auth_OpenID_WebResponse($code = null, $headers = null, + $body = null) + { + if ($code) { + $this->code = $code; + } + + if ($headers !== null) { + $this->headers = $headers; + } else { + $this->headers = array(); + } + + if ($body !== null) { + $this->body = $body; + } + } +} + +/** + * Responsible for the signature of query data and the verification of + * OpenID signature values. + * + * @package OpenID + */ +class Auth_OpenID_Signatory { + + // = 14 * 24 * 60 * 60; # 14 days, in seconds + var $SECRET_LIFETIME = 1209600; + + // keys have a bogus server URL in them because the filestore + // really does expect that key to be a URL. This seems a little + // silly for the server store, since I expect there to be only one + // server URL. + var $normal_key = 'http://localhost/|normal'; + var $dumb_key = 'http://localhost/|dumb'; + + /** + * Create a new signatory using a given store. + */ + function Auth_OpenID_Signatory(&$store) + { + // assert store is not None + $this->store =& $store; + } + + /** + * Verify, using a given association handle, a signature with + * signed key-value pairs from an HTTP request. + */ + function verify($assoc_handle, $message) + { + $assoc = $this->getAssociation($assoc_handle, true); + if (!$assoc) { + // oidutil.log("failed to get assoc with handle %r to verify sig %r" + // % (assoc_handle, sig)) + return false; + } + + return $assoc->checkMessageSignature($message); + } + + /** + * Given a response, sign the fields in the response's 'signed' + * list, and insert the signature into the response. + */ + function sign($response) + { + $signed_response = $response; + $assoc_handle = $response->request->assoc_handle; + + if ($assoc_handle) { + // normal mode + $assoc = $this->getAssociation($assoc_handle, false, false); + if (!$assoc || ($assoc->getExpiresIn() <= 0)) { + // fall back to dumb mode + $signed_response->fields->setArg(Auth_OpenID_OPENID_NS, + 'invalidate_handle', $assoc_handle); + $assoc_type = ($assoc ? $assoc->assoc_type : 'HMAC-SHA1'); + + if ($assoc && ($assoc->getExpiresIn() <= 0)) { + $this->invalidate($assoc_handle, false); + } + + $assoc = $this->createAssociation(true, $assoc_type); + } + } else { + // dumb mode. + $assoc = $this->createAssociation(true); + } + + $signed_response->fields = $assoc->signMessage( + $signed_response->fields); + return $signed_response; + } + + /** + * Make a new association. + */ + function createAssociation($dumb = true, $assoc_type = 'HMAC-SHA1') + { + $secret = Auth_OpenID_CryptUtil::getBytes( + Auth_OpenID_getSecretSize($assoc_type)); + + $uniq = base64_encode(Auth_OpenID_CryptUtil::getBytes(4)); + $handle = sprintf('{%s}{%x}{%s}', $assoc_type, intval(time()), $uniq); + + $assoc = Auth_OpenID_Association::fromExpiresIn( + $this->SECRET_LIFETIME, $handle, $secret, $assoc_type); + + if ($dumb) { + $key = $this->dumb_key; + } else { + $key = $this->normal_key; + } + + $this->store->storeAssociation($key, $assoc); + return $assoc; + } + + /** + * Given an association handle, get the association from the + * store, or return a ServerError or null if something goes wrong. + */ + function getAssociation($assoc_handle, $dumb, $check_expiration=true) + { + if ($assoc_handle === null) { + return new Auth_OpenID_ServerError(null, + "assoc_handle must not be null"); + } + + if ($dumb) { + $key = $this->dumb_key; + } else { + $key = $this->normal_key; + } + + $assoc = $this->store->getAssociation($key, $assoc_handle); + + if (($assoc !== null) && ($assoc->getExpiresIn() <= 0)) { + if ($check_expiration) { + $this->store->removeAssociation($key, $assoc_handle); + $assoc = null; + } + } + + return $assoc; + } + + /** + * Invalidate a given association handle. + */ + function invalidate($assoc_handle, $dumb) + { + if ($dumb) { + $key = $this->dumb_key; + } else { + $key = $this->normal_key; + } + $this->store->removeAssociation($key, $assoc_handle); + } +} + +/** + * Encode an {@link Auth_OpenID_ServerResponse} to an + * {@link Auth_OpenID_WebResponse}. + * + * @package OpenID + */ +class Auth_OpenID_Encoder { + + var $responseFactory = 'Auth_OpenID_WebResponse'; + + /** + * Encode an {@link Auth_OpenID_ServerResponse} and return an + * {@link Auth_OpenID_WebResponse}. + */ + function encode(&$response) + { + $cls = $this->responseFactory; + + $encode_as = $response->whichEncoding(); + 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) { + $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); + } + return $wr; + } +} + +/** + * An encoder which also takes care of signing fields when required. + * + * @package OpenID + */ +class Auth_OpenID_SigningEncoder extends Auth_OpenID_Encoder { + + function Auth_OpenID_SigningEncoder(&$signatory) + { + $this->signatory =& $signatory; + } + + /** + * Sign an {@link Auth_OpenID_ServerResponse} and return an + * {@link Auth_OpenID_WebResponse}. + */ + 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') && + $response->needsSigning()) { + + if (!$this->signatory) { + return new Auth_OpenID_ServerError(null, + "Must have a store to sign request"); + } + + if ($response->fields->hasKey(Auth_OpenID_OPENID_NS, 'sig')) { + return new Auth_OpenID_AlreadySigned($response); + } + $response = $this->signatory->sign($response); + } + + return parent::encode($response); + } +} + +/** + * Decode an incoming query into an Auth_OpenID_Request. + * + * @package OpenID + */ +class Auth_OpenID_Decoder { + + function Auth_OpenID_Decoder(&$server) + { + $this->server =& $server; + + $this->handlers = array( + 'checkid_setup' => 'Auth_OpenID_CheckIDRequest', + 'checkid_immediate' => 'Auth_OpenID_CheckIDRequest', + 'check_authentication' => 'Auth_OpenID_CheckAuthRequest', + 'associate' => 'Auth_OpenID_AssociateRequest' + ); + } + + /** + * Given an HTTP query in an array (key-value pairs), decode it + * into an Auth_OpenID_Request object. + */ + function decode($query) + { + if (!$query) { + return null; + } + + $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)); + + if (!is_a($handlerCls, 'Auth_OpenID_ServerError')) { + return call_user_func_array(array($handlerCls, 'fromMessage'), + array($message, $this->server)); + } else { + return $handlerCls; + } + } + + 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("Unrecognized OpenID mode %s", $mode)); + } +} + +/** + * An error that indicates an encoding problem occurred. + * + * @package OpenID + */ +class Auth_OpenID_EncodingError { + function Auth_OpenID_EncodingError(&$response) + { + $this->response =& $response; + } +} + +/** + * An error that indicates that a response was already signed. + * + * @package OpenID + */ +class Auth_OpenID_AlreadySigned extends Auth_OpenID_EncodingError { + // This response is already signed. +} + +/** + * An error that indicates that the given return_to is not under the + * given trust_root. + * + * @package OpenID + */ +class Auth_OpenID_UntrustedReturnURL extends Auth_OpenID_ServerError { + function Auth_OpenID_UntrustedReturnURL($message, $return_to, + $trust_root) + { + parent::Auth_OpenID_ServerError($message, "Untrusted return_to URL"); + $this->return_to = $return_to; + $this->trust_root = $trust_root; + } + + function toString() + { + return sprintf("return_to %s not under trust_root %s", + $this->return_to, $this->trust_root); + } +} + +/** + * I handle requests for an OpenID server. + * + * Some types of requests (those which are not checkid requests) may + * be handed to my {@link handleRequest} method, and I will take care + * of it and return a response. + * + * For your convenience, I also provide an interface to {@link + * Auth_OpenID_Decoder::decode()} and {@link + * Auth_OpenID_SigningEncoder::encode()} through my methods {@link + * decodeRequest} and {@link encodeResponse}. + * + * All my state is encapsulated in an {@link Auth_OpenID_OpenIDStore}. + * + * Example: + * + * <pre> $oserver = new Auth_OpenID_Server(Auth_OpenID_FileStore($data_path), + * "http://example.com/op"); + * $request = $oserver->decodeRequest(); + * if (in_array($request->mode, array('checkid_immediate', + * 'checkid_setup'))) { + * if ($app->isAuthorized($request->identity, $request->trust_root)) { + * $response = $request->answer(true); + * } else if ($request->immediate) { + * $response = $request->answer(false); + * } else { + * $app->showDecidePage($request); + * return; + * } + * } else { + * $response = $oserver->handleRequest($request); + * } + * + * $webresponse = $oserver->encode($response);</pre> + * + * @package OpenID + */ +class Auth_OpenID_Server { + function Auth_OpenID_Server(&$store, $op_endpoint=null) + { + $this->store =& $store; + $this->signatory =& new Auth_OpenID_Signatory($this->store); + $this->encoder =& new Auth_OpenID_SigningEncoder($this->signatory); + $this->decoder =& new Auth_OpenID_Decoder($this); + $this->op_endpoint = $op_endpoint; + $this->negotiator =& Auth_OpenID_getDefaultNegotiator(); + } + + /** + * Handle a request. Given an {@link Auth_OpenID_Request} object, + * call the appropriate {@link Auth_OpenID_Server} method to + * process the request and generate a response. + * + * @param Auth_OpenID_Request $request An {@link Auth_OpenID_Request} + * returned by {@link Auth_OpenID_Server::decodeRequest()}. + * + * @return Auth_OpenID_ServerResponse $response A response object + * capable of generating a user-agent reply. + */ + function handleRequest($request) + { + if (method_exists($this, "openid_" . $request->mode)) { + $handler = array($this, "openid_" . $request->mode); + return call_user_func($handler, $request); + } + return null; + } + + /** + * The callback for 'check_authentication' messages. + */ + function openid_check_authentication(&$request) + { + return $request->answer($this->signatory); + } + + /** + * The callback for 'associate' messages. + */ + function openid_associate(&$request) + { + $assoc_type = $request->assoc_type; + $session_type = $request->session->session_type; + if ($this->negotiator->isAllowed($assoc_type, $session_type)) { + $assoc = $this->signatory->createAssociation(false, + $assoc_type); + return $request->answer($assoc); + } else { + $message = sprintf('Association type %s is not supported with '. + 'session type %s', $assoc_type, $session_type); + list($preferred_assoc_type, $preferred_session_type) = + $this->negotiator->getAllowedType(); + return $request->answerUnsupported($message, + $preferred_assoc_type, + $preferred_session_type); + } + } + + /** + * Encodes as response in the appropriate format suitable for + * sending to the user agent. + */ + function encodeResponse(&$response) + { + return $this->encoder->encode($response); + } + + /** + * Decodes a query args array into the appropriate + * {@link Auth_OpenID_Request} object. + */ + function decodeRequest($query=null) + { + if ($query === null) { + $query = Auth_OpenID::getQuery(); + } + + return $this->decoder->decode($query); + } +} + +?> diff --git a/Auth/OpenID/ServerRequest.php b/Auth/OpenID/ServerRequest.php new file mode 100644 index 0000000..33a8556 --- /dev/null +++ b/Auth/OpenID/ServerRequest.php @@ -0,0 +1,37 @@ +<?php +/** + * OpenID Server Request + * + * @see Auth_OpenID_Server + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Imports + */ +require_once "Auth/OpenID.php"; + +/** + * Object that holds the state of a request to the OpenID server + * + * With accessor functions to get at the internal request data. + * + * @see Auth_OpenID_Server + * @package OpenID + */ +class Auth_OpenID_ServerRequest { + function Auth_OpenID_ServerRequest() + { + $this->mode = null; + } +} + +?>
\ No newline at end of file diff --git a/Auth/OpenID/TrustRoot.php b/Auth/OpenID/TrustRoot.php new file mode 100644 index 0000000..4919a60 --- /dev/null +++ b/Auth/OpenID/TrustRoot.php @@ -0,0 +1,462 @@ +<?php +/** + * Functions for dealing with OpenID trust roots + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @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. + * + * @access private + */ +define('Auth_OpenID___TLDs', + '/\.(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. + * + * @static + * + * @access private + * + * @param string $trust_root The url to parse + * + * @return mixed $parsed Either an associative array of trust root + * parts or false if parsing failed. + */ + 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); + if (array_intersect($keys, $required_parts) != $required_parts) { + return false; + } + + if (array_intersect($keys, $forbidden_parts) != array()) { + return false; + } + + if (!preg_match(Auth_OpenID___HostSegmentRe, $parts['host'])) { + return false; + } + + $scheme = strtolower($parts['scheme']); + $allowed_schemes = array('http', 'https'); + if (!in_array($scheme, $allowed_schemes)) { + return false; + } + $parts['scheme'] = $scheme; + + $host = strtolower($parts['host']); + $hostparts = explode('*', $host); + switch (count($hostparts)) { + case 1: + $parts['wildcard'] = false; + break; + case 2: + if ($hostparts[0] || + ($hostparts[1] && substr($hostparts[1], 0, 1) != '.')) { + return false; + } + $host = $hostparts[1]; + $parts['wildcard'] = true; + break; + default: + return false; + } + if (strpos($host, ':') !== false) { + return false; + } + + $parts['host'] = $host; + + if (isset($parts['path'])) { + $path = strtolower($parts['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; + } + + /** + * Is this trust root sane? + * + * A trust root is sane if it is syntactically valid and it has a + * reasonable domain name. Specifically, the domain name must be + * more than one level below a standard TLD or more than two + * levels below a two-letter tld. + * + * For example, '*.com' is not a sane trust root, but '*.foo.com' + * is. '*.co.uk' is not sane, but '*.bbc.co.uk' is. + * + * This check is not always correct, but it attempts to err on the + * side of marking sane trust roots insane instead of marking + * insane trust roots sane. For example, 'kink.fm' is marked as + * insane even though it "should" (for some meaning of should) be + * marked sane. + * + * This function should be used when creating OpenID servers to + * alert the users of the server when a consumer attempts to get + * the user to accept a suspicious trust root. + * + * @static + * @param string $trust_root The trust root to check + * @return bool $sanity Whether the trust root looks OK + */ + function isSane($trust_root) + { + $parts = Auth_OpenID_TrustRoot::_parse($trust_root); + if ($parts === false) { + return false; + } + + // Localhost is a special case + 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. + preg_match(Auth_OpenID___TLDs, $parts['host'], $matches); + if (!$matches) { + return false; + } + $tld = $matches[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; + } + + /** + * Does this URL match the given trust root? + * + * Return whether the URL falls under the given trust root. This + * does not check whether the trust root is sane. If the URL or + * trust root do not parse, this function will return false. + * + * @param string $trust_root The trust root to match against + * + * @param string $url The URL to check + * + * @return bool $matches Whether the URL matches against the + * trust root + */ + function match($trust_root, $url) + { + $trust_root_parsed = Auth_OpenID_TrustRoot::_parse($trust_root); + $url_parsed = Auth_OpenID_TrustRoot::_parse($url); + if (!$trust_root_parsed || !$url_parsed) { + return false; + } + + // Check hosts matching + if ($url_parsed['wildcard']) { + return false; + } + if ($trust_root_parsed['wildcard']) { + $host_tail = $trust_root_parsed['host']; + $host = $url_parsed['host']; + if ($host_tail && + substr($host, -(strlen($host_tail))) != $host_tail && + substr($host_tail, 1) != $host) { + return false; + } + } else { + if ($trust_root_parsed['host'] != $url_parsed['host']) { + return false; + } + } + + // Check path and query matching + $base_path = $trust_root_parsed['path']; + $path = $url_parsed['path']; + if (!isset($trust_root_parsed['query'])) { + 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']; + $query = @$url_parsed['query']; + $qplus = substr($query, 0, strlen($base_query) + 1); + $bqplus = $base_query . '&'; + if ($base_path != $path || + ($base_query != $query && $qplus != $bqplus)) { + return false; + } + } + + // The port and scheme need to match exactly + return ($trust_root_parsed['scheme'] == $url_parsed['scheme'] && + $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/Auth/OpenID/URINorm.php b/Auth/OpenID/URINorm.php new file mode 100644 index 0000000..f821d83 --- /dev/null +++ b/Auth/OpenID/URINorm.php @@ -0,0 +1,249 @@ +<?php + +/** + * URI normalization routines. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'Auth/Yadis/Misc.php'; + +// from appendix B of rfc 3986 (http://www.ietf.org/rfc/rfc3986.txt) +function Auth_OpenID_getURIPattern() +{ + return '&^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?&'; +} + +function Auth_OpenID_getAuthorityPattern() +{ + return '/^([^@]*@)?([^:]*)(:.*)?/'; +} + +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(); + for ($i = 0; $i < 256; $i++) { + $_unreserved[$i] = false; + } + + for ($i = ord('A'); $i <= ord('Z'); $i++) { + $_unreserved[$i] = true; + } + + for ($i = ord('0'); $i <= ord('9'); $i++) { + $_unreserved[$i] = true; + } + + for ($i = ord('a'); $i <= ord('z'); $i++) { + $_unreserved[$i] = true; + } + + $_unreserved[ord('-')] = true; + $_unreserved[ord('.')] = true; + $_unreserved[ord('_')] = true; + $_unreserved[ord('~')] = true; + + return $_unreserved; +} + +function Auth_OpenID_getEscapeRE() +{ + $parts = array(); + foreach (array_merge(Auth_Yadis_getUCSChars(), + Auth_Yadis_getIPrivateChars()) as $pair) { + list($m, $n) = $pair; + $parts[] = sprintf("%s-%s", chr($m), chr($n)); + } + + return sprintf('[%s]', implode('', $parts)); +} + +function Auth_OpenID_pct_encoded_replace_unreserved($mo) +{ + $_unreserved = Auth_OpenID_getUnreserved(); + + $i = intval($mo[1], 16); + if ($_unreserved[$i]) { + return chr($i); + } else { + return strtoupper($mo[0]); + } + + return $mo[0]; +} + +function Auth_OpenID_pct_encoded_replace($mo) +{ + return chr(intval($mo[1], 16)); +} + +function Auth_OpenID_remove_dot_segments($path) +{ + $result_segments = array(); + + while ($path) { + if (Auth_Yadis_startswith($path, '../')) { + $path = substr($path, 3); + } else if (Auth_Yadis_startswith($path, './')) { + $path = substr($path, 2); + } else if (Auth_Yadis_startswith($path, '/./')) { + $path = substr($path, 2); + } else if ($path == '/.') { + $path = '/'; + } else if (Auth_Yadis_startswith($path, '/../')) { + $path = substr($path, 3); + if ($result_segments) { + array_pop($result_segments); + } + } else if ($path == '/..') { + $path = '/'; + if ($result_segments) { + array_pop($result_segments); + } + } else if (($path == '..') || + ($path == '.')) { + $path = ''; + } else { + $i = 0; + if ($path[0] == '/') { + $i = 1; + } + $i = strpos($path, '/', $i); + if ($i === false) { + $i = strlen($path); + } + $result_segments[] = substr($path, 0, $i); + $path = substr($path, $i); + } + } + + return implode('', $result_segments); +} + +function Auth_OpenID_urinorm($uri) +{ + $uri_matches = array(); + preg_match(Auth_OpenID_getURIPattern(), $uri, $uri_matches); + + if (count($uri_matches) < 9) { + for ($i = count($uri_matches); $i <= 9; $i++) { + $uri_matches[] = ''; + } + } + + $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); + } + + $scheme = $uri_matches[2]; + if ($scheme === '') { + // No scheme specified + return null; + } + + $scheme = strtolower($scheme); + if (!in_array($scheme, array('http', 'https'))) { + // Not an absolute HTTP or HTTPS URI + return null; + } + + $authority = $uri_matches[4]; + if ($authority === '') { + // Not an absolute URI + return null; + } + + $authority_matches = array(); + preg_match(Auth_OpenID_getAuthorityPattern(), + $authority, $authority_matches); + if (count($authority_matches) === 0) { + // URI does not have a valid authority + return null; + } + + if (count($authority_matches) < 4) { + for ($i = count($authority_matches); $i <= 4; $i++) { + $authority_matches[] = ''; + } + } + + list($_whole, $userinfo, $host, $port) = $authority_matches; + + if ($userinfo === null) { + $userinfo = ''; + } + + if (strpos($host, '%') !== -1) { + $host = strtolower($host); + $host = preg_replace_callback( + Auth_OpenID_getEncodedPattern(), + 'Auth_OpenID_pct_encoded_replace', $host); + // NO IDNA. + // $host = unicode($host, 'utf-8').encode('idna'); + } else { + $host = strtolower($host); + } + + if ($port) { + if (($port == ':') || + ($scheme == 'http' && $port == ':80') || + ($scheme == 'https' && $port == ':443')) { + $port = ''; + } + } else { + $port = ''; + } + + $authority = $userinfo . $host . $port; + + $path = $uri_matches[5]; + $path = preg_replace_callback( + Auth_OpenID_getEncodedPattern(), + 'Auth_OpenID_pct_encoded_replace_unreserved', $path); + + $path = Auth_OpenID_remove_dot_segments($path); + if (!$path) { + $path = '/'; + } + + $query = $uri_matches[6]; + if ($query === null) { + $query = ''; + } + + $fragment = $uri_matches[8]; + if ($fragment === null) { + $fragment = ''; + } + + return $scheme . '://' . $authority . $path . $query . $fragment; +} + +?> diff --git a/Auth/Yadis/HTTPFetcher.php b/Auth/Yadis/HTTPFetcher.php new file mode 100644 index 0000000..963b9a4 --- /dev/null +++ b/Auth/Yadis/HTTPFetcher.php @@ -0,0 +1,147 @@ +<?php + +/** + * This module contains the HTTP fetcher interface + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @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) + { + $this->final_url = $final_url; + $this->status = $status; + $this->headers = $headers; + $this->body = $body; + } +} + +/** + * This class is the interface for HTTP fetchers the Yadis library + * uses. This interface is only important if you need to write a new + * fetcher for some reason. + * + * @access private + * @package OpenID + */ +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. + * + * By default, will attempt to fetch any http or https URL. + */ + function allowedURL($url) + { + return $this->URLHasAllowedScheme($url); + } + + /** + * Does this fetcher implementation (and runtime) support fetching + * HTTPS URLs? May inspect the runtime environment. + * + * @return bool $support True if this fetcher supports HTTPS + * fetching; false if not. + */ + function supportsSSL() + { + trigger_error("not implemented", E_USER_ERROR); + } + + /** + * Is this an https URL? + * + * @access private + */ + function isHTTPS($url) + { + return (bool)preg_match('/^https:\/\//i', $url); + } + + /** + * Is this an http or https URL? + * + * @access private + */ + function URLHasAllowedScheme($url) + { + return (bool)preg_match('/^https?:\/\//i', $url); + } + + /** + * @access private + */ + function _findRedirect($headers) + { + foreach ($headers as $line) { + if (strpos(strtolower($line), "location: ") === 0) { + $parts = explode(" ", $line, 2); + return $parts[1]; + } + } + return null; + } + + /** + * Fetches the specified URL using optional extra headers and + * returns the server's response. + * + * @param string $url The URL to be fetched. + * @param array $extra_headers An array of header strings + * (e.g. "Accept: text/html"). + * @return mixed $result An array of ($code, $url, $headers, + * $body) if the URL could be fetched; null if the URL does not + * pass the URLHasAllowedScheme check or if the server's response + * is malformed. + */ + function get($url, $headers = null) + { + trigger_error("not implemented", E_USER_ERROR); + } +} + +?>
\ No newline at end of file diff --git a/Auth/Yadis/Manager.php b/Auth/Yadis/Manager.php new file mode 100644 index 0000000..d50cf7a --- /dev/null +++ b/Auth/Yadis/Manager.php @@ -0,0 +1,529 @@ +<?php + +/** + * Yadis service manager to be used during yadis-driven authentication + * attempts. + * + * @package OpenID + */ + +/** + * The base session class used by the Auth_Yadis_Manager. This + * class wraps the default PHP session machinery and should be + * subclassed if your application doesn't use PHP sessioning. + * + * @package OpenID + */ +class Auth_Yadis_PHPSession { + /** + * Set a session key/value pair. + * + * @param string $name The name of the session key to add. + * @param string $value The value to add to the session. + */ + function set($name, $value) + { + $_SESSION[$name] = $value; + } + + /** + * Get a key's value from the session. + * + * @param string $name The name of the key to retrieve. + * @param string $default The optional value to return if the key + * is not found in the session. + * @return string $result The key's value in the session or + * $default if it isn't found. + */ + function get($name, $default=null) + { + if (array_key_exists($name, $_SESSION)) { + return $_SESSION[$name]; + } else { + return $default; + } + } + + /** + * Remove a key/value pair from the session. + * + * @param string $name The name of the key to remove. + */ + function del($name) + { + unset($_SESSION[$name]); + } + + /** + * Return the contents of the session in array form. + */ + function contents() + { + return $_SESSION; + } +} + +/** + * A session helper class designed to translate between arrays and + * objects. Note that the class used must have a constructor that + * takes no parameters. This is not a general solution, but it works + * for dumb objects that just need to have attributes set. The idea + * is that you'll subclass this and override $this->check($data) -> + * bool to implement your own session data validation. + * + * @package OpenID + */ +class Auth_Yadis_SessionLoader { + /** + * Override this. + * + * @access private + */ + function check($data) + { + return true; + } + + /** + * Given a session data value (an array), this creates an object + * (returned by $this->newObject()) whose attributes and values + * are those in $data. Returns null if $data lacks keys found in + * $this->requiredKeys(). Returns null if $this->check($data) + * evaluates to false. Returns null if $this->newObject() + * evaluates to false. + * + * @access private + */ + function fromSession($data) + { + if (!$data) { + return null; + } + + $required = $this->requiredKeys(); + + foreach ($required as $k) { + if (!array_key_exists($k, $data)) { + return null; + } + } + + if (!$this->check($data)) { + return null; + } + + $data = array_merge($data, $this->prepareForLoad($data)); + $obj = $this->newObject($data); + + if (!$obj) { + return null; + } + + foreach ($required as $k) { + $obj->$k = $data[$k]; + } + + return $obj; + } + + /** + * Prepares the data array by making any necessary changes. + * Returns an array whose keys and values will be used to update + * the original data array before calling $this->newObject($data). + * + * @access private + */ + function prepareForLoad($data) + { + return array(); + } + + /** + * Returns a new instance of this loader's class, using the + * session data to construct it if necessary. The object need + * only be created; $this->fromSession() will take care of setting + * the object's attributes. + * + * @access private + */ + function newObject($data) + { + return null; + } + + /** + * Returns an array of keys and values built from the attributes + * of $obj. If $this->prepareForSave($obj) returns an array, its keys + * and values are used to update the $data array of attributes + * from $obj. + * + * @access private + */ + function toSession($obj) + { + $data = array(); + foreach ($obj as $k => $v) { + $data[$k] = $v; + } + + $extra = $this->prepareForSave($obj); + + if ($extra && is_array($extra)) { + foreach ($extra as $k => $v) { + $data[$k] = $v; + } + } + + return $data; + } + + /** + * Override this. + * + * @access private + */ + function prepareForSave($obj) + { + return array(); + } +} + +/** + * A concrete loader implementation for Auth_OpenID_ServiceEndpoints. + * + * @package OpenID + */ +class Auth_OpenID_ServiceEndpointLoader extends Auth_Yadis_SessionLoader { + function newObject($data) + { + return new Auth_OpenID_ServiceEndpoint(); + } + + function requiredKeys() + { + $obj = new Auth_OpenID_ServiceEndpoint(); + $data = array(); + foreach ($obj as $k => $v) { + $data[] = $k; + } + return $data; + } + + function check($data) + { + return is_array($data['type_uris']); + } +} + +/** + * A concrete loader implementation for Auth_Yadis_Managers. + * + * @package OpenID + */ +class Auth_Yadis_ManagerLoader extends Auth_Yadis_SessionLoader { + function requiredKeys() + { + return array('starting_url', + 'yadis_url', + 'services', + 'session_key', + '_current', + 'stale'); + } + + function newObject($data) + { + return new Auth_Yadis_Manager($data['starting_url'], + $data['yadis_url'], + $data['services'], + $data['session_key']); + } + + function check($data) + { + return is_array($data['services']); + } + + function prepareForLoad($data) + { + $loader = new Auth_OpenID_ServiceEndpointLoader(); + $services = array(); + foreach ($data['services'] as $s) { + $services[] = $loader->fromSession($s); + } + return array('services' => $services); + } + + function prepareForSave($obj) + { + $loader = new Auth_OpenID_ServiceEndpointLoader(); + $services = array(); + foreach ($obj->services as $s) { + $services[] = $loader->toSession($s); + } + return array('services' => $services); + } +} + +/** + * The Yadis service manager which stores state in a session and + * iterates over <Service> elements in a Yadis XRDS document and lets + * a caller attempt to use each one. This is used by the Yadis + * library internally. + * + * @package OpenID + */ +class Auth_Yadis_Manager { + + /** + * Intialize a new yadis service manager. + * + * @access private + */ + function Auth_Yadis_Manager($starting_url, $yadis_url, + $services, $session_key) + { + // The URL that was used to initiate the Yadis protocol + $this->starting_url = $starting_url; + + // The URL after following redirects (the identifier) + $this->yadis_url = $yadis_url; + + // List of service elements + $this->services = $services; + + $this->session_key = $session_key; + + // Reference to the current service object + $this->_current = null; + + // Stale flag for cleanup if PHP lib has trouble. + $this->stale = false; + } + + /** + * @access private + */ + function length() + { + // How many untried services remain? + return count($this->services); + } + + /** + * Return the next service + * + * $this->current() will continue to return that service until the + * next call to this method. + */ + function nextService() + { + + if ($this->services) { + $this->_current = array_shift($this->services); + } else { + $this->_current = null; + } + + return $this->_current; + } + + /** + * @access private + */ + function current() + { + // Return the current service. + // Returns None if there are no services left. + return $this->_current; + } + + /** + * @access private + */ + function forURL($url) + { + return in_array($url, array($this->starting_url, $this->yadis_url)); + } + + /** + * @access private + */ + function started() + { + // Has the first service been returned? + return $this->_current !== null; + } +} + +/** + * State management for discovery. + * + * 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 .cleanup() to clean up the + * session state. + * + * @package OpenID + */ +class Auth_Yadis_Discovery { + + /** + * @access private + */ + var $DEFAULT_SUFFIX = 'auth'; + + /** + * @access private + */ + var $PREFIX = '_yadis_services_'; + + /** + * Initialize a discovery object. + * + * @param Auth_Yadis_PHPSession $session An object which + * implements the Auth_Yadis_PHPSession API. + * @param string $url The URL on which to attempt discovery. + * @param string $session_key_suffix The optional session key + * suffix override. + */ + function Auth_Yadis_Discovery(&$session, $url, + $session_key_suffix = null) + { + /// Initialize a discovery object + $this->session =& $session; + $this->url = $url; + if ($session_key_suffix === null) { + $session_key_suffix = $this->DEFAULT_SUFFIX; + } + + $this->session_key_suffix = $session_key_suffix; + $this->session_key = $this->PREFIX . $this->session_key_suffix; + } + + /** + * Return the next authentication service for the pair of + * user_input and session. This function handles fallback. + */ + function getNextService($discover_cb, &$fetcher) + { + $manager = $this->getManager(); + if (!$manager || (!$manager->services)) { + $this->destroyManager(); + + list($yadis_url, $services) = call_user_func($discover_cb, + $this->url, + $fetcher); + + $manager = $this->createManager($services, $yadis_url); + } + + if ($manager) { + $loader = new Auth_Yadis_ManagerLoader(); + $service = $manager->nextService(); + $this->session->set($this->session_key, + serialize($loader->toSession($manager))); + } else { + $service = null; + } + + return $service; + } + + /** + * 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($force=false) + { + $manager = $this->getManager($force); + if ($manager) { + $service = $manager->current(); + $this->destroyManager($force); + } else { + $service = null; + } + + return $service; + } + + /** + * @access private + */ + function getSessionKey() + { + // Get the session key for this starting URL and suffix + return $this->PREFIX . $this->session_key_suffix; + } + + /** + * @access private + * + * @param $force True if the manager should be returned regardless + * of whether it's a manager for $this->url. + */ + function &getManager($force=false) + { + // Extract the YadisServiceManager for this object's URL and + // suffix from the session. + + $manager_str = $this->session->get($this->getSessionKey()); + $manager = null; + + if ($manager_str !== null) { + $loader = new Auth_Yadis_ManagerLoader(); + $manager = $loader->fromSession(unserialize($manager_str)); + } + + if ($manager && ($manager->forURL($this->url) || $force)) { + return $manager; + } else { + $unused = null; + return $unused; + } + } + + /** + * @access private + */ + function &createManager($services, $yadis_url = null) + { + $key = $this->getSessionKey(); + if ($this->getManager()) { + return $this->getManager(); + } + + if ($services) { + $loader = new Auth_Yadis_ManagerLoader(); + $manager = new Auth_Yadis_Manager($this->url, $yadis_url, + $services, $key); + $this->session->set($this->session_key, + serialize($loader->toSession($manager))); + return $manager; + } else { + // Oh, PHP. + $unused = null; + return $unused; + } + } + + /** + * @access private + * + * @param $force True if the manager should be deleted regardless + * of whether it's a manager for $this->url. + */ + function destroyManager($force=false) + { + if ($this->getManager($force) !== null) { + $key = $this->getSessionKey(); + $this->session->del($key); + } + } +} + +?>
\ No newline at end of file diff --git a/Auth/Yadis/Misc.php b/Auth/Yadis/Misc.php new file mode 100644 index 0000000..1134a4f --- /dev/null +++ b/Auth/Yadis/Misc.php @@ -0,0 +1,59 @@ +<?php + +/** + * Miscellaneous utility values and functions for OpenID and Yadis. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +function Auth_Yadis_getUCSChars() +{ + return array( + array(0xA0, 0xD7FF), + array(0xF900, 0xFDCF), + array(0xFDF0, 0xFFEF), + array(0x10000, 0x1FFFD), + array(0x20000, 0x2FFFD), + array(0x30000, 0x3FFFD), + array(0x40000, 0x4FFFD), + array(0x50000, 0x5FFFD), + array(0x60000, 0x6FFFD), + array(0x70000, 0x7FFFD), + array(0x80000, 0x8FFFD), + array(0x90000, 0x9FFFD), + array(0xA0000, 0xAFFFD), + array(0xB0000, 0xBFFFD), + array(0xC0000, 0xCFFFD), + array(0xD0000, 0xDFFFD), + array(0xE1000, 0xEFFFD) + ); +} + +function Auth_Yadis_getIPrivateChars() +{ + return array( + array(0xE000, 0xF8FF), + array(0xF0000, 0xFFFFD), + array(0x100000, 0x10FFFD) + ); +} + +function Auth_Yadis_pct_escape_unicode($char_match) +{ + $c = $char_match[0]; + $result = ""; + for ($i = 0; $i < strlen($c); $i++) { + $result .= "%".sprintf("%X", ord($c[$i])); + } + return $result; +} + +function Auth_Yadis_startswith($s, $stuff) +{ + return strpos($s, $stuff) === 0; +} + +?>
\ No newline at end of file diff --git a/Auth/Yadis/ParanoidHTTPFetcher.php b/Auth/Yadis/ParanoidHTTPFetcher.php new file mode 100644 index 0000000..6a41826 --- /dev/null +++ b/Auth/Yadis/ParanoidHTTPFetcher.php @@ -0,0 +1,226 @@ +<?php + +/** + * This module contains the CURL-based HTTP fetcher implementation. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Interface import + */ +require_once "Auth/Yadis/HTTPFetcher.php"; + +require_once "Auth/OpenID.php"; + +/** + * A paranoid {@link Auth_Yadis_HTTPFetcher} class which uses CURL + * for fetching. + * + * @package OpenID + */ +class Auth_Yadis_ParanoidHTTPFetcher extends Auth_Yadis_HTTPFetcher { + function Auth_Yadis_ParanoidHTTPFetcher() + { + $this->reset(); + } + + function reset() + { + $this->headers = array(); + $this->data = ""; + } + + /** + * @access private + */ + function _writeHeader($ch, $header) + { + array_push($this->headers, rtrim($header)); + return strlen($header); + } + + /** + * @access private + */ + function _writeData($ch, $data) + { + if (strlen($this->data) > 1024*Auth_OpenID_FETCHER_MAX_RESPONSE_KB) { + return 0; + } else { + $this->data .= $data; + return strlen($data); + } + } + + /** + * Does this fetcher support SSL URLs? + */ + function supportsSSL() + { + $v = curl_version(); + 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->canFetchURL($url)) { + return null; + } + + $stop = time() + $this->timeout; + $off = $this->timeout; + + $redir = true; + + while ($redir && ($off > 0)) { + $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; + } + + curl_setopt($c, CURLOPT_WRITEFUNCTION, + array(&$this, "_writeData")); + curl_setopt($c, CURLOPT_HEADERFUNCTION, + array(&$this, "_writeHeader")); + + if ($extra_headers) { + 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); + + curl_exec($c); + + $code = curl_getinfo($c, CURLINFO_HTTP_CODE); + $body = $this->data; + $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; + } + + if (in_array($code, array(301, 302, 303, 307))) { + $url = $this->_findRedirect($headers); + $redir = true; + } else { + $redir = false; + curl_close($c); + + $new_headers = array(); + + foreach ($headers as $header) { + 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); + } + + $off = $stop - time(); + } + + return null; + } + + function post($url, $body, $extra_headers = null) + { + if (!$this->canFetchURL($url)) { + return null; + } + + $this->reset(); + + $c = curl_init(); + + 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); + curl_setopt($c, CURLOPT_URL, $url); + curl_setopt($c, CURLOPT_WRITEFUNCTION, + array(&$this, "_writeData")); + + curl_exec($c); + + $code = curl_getinfo($c, CURLINFO_HTTP_CODE); + + if (!$code) { + Auth_OpenID::log("Got no response code when fetching %s", $url); + return null; + } + + $body = $this->data; + + curl_close($c); + + $new_headers = $extra_headers; + + foreach ($this->headers as $header) { + 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); + } +} + +?>
\ No newline at end of file diff --git a/Auth/Yadis/ParseHTML.php b/Auth/Yadis/ParseHTML.php new file mode 100644 index 0000000..297ccbd --- /dev/null +++ b/Auth/Yadis/ParseHTML.php @@ -0,0 +1,259 @@ +<?php + +/** + * This is the HTML pseudo-parser for the Yadis library. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * This class is responsible for scanning an HTML string to find META + * tags and their attributes. This is used by the Yadis discovery + * process. This class must be instantiated to be used. + * + * @package OpenID + */ +class Auth_Yadis_ParseHTML { + + /** + * @access private + */ + var $_re_flags = "si"; + + /** + * @access private + */ + var $_removed_re = + "<!--.*?-->|<!\[CDATA\[.*?\]\]>|<script\b(?!:)[^>]*>.*?<\/script>"; + + /** + * @access private + */ + var $_tag_expr = "<%s%s(?:\s.*?)?%s>"; + + /** + * @access private + */ + var $_attr_find = '\b([-\w]+)=(".*?"|\'.*?\'|.+?)[\/\s>]'; + + function Auth_Yadis_ParseHTML() + { + $this->_attr_find = sprintf("/%s/%s", + $this->_attr_find, + $this->_re_flags); + + $this->_removed_re = sprintf("/%s/%s", + $this->_removed_re, + $this->_re_flags); + + $this->_entity_replacements = array( + 'amp' => '&', + 'lt' => '<', + 'gt' => '>', + 'quot' => '"' + ); + + $this->_ent_replace = + sprintf("&(%s);", implode("|", + $this->_entity_replacements)); + } + + /** + * Replace HTML entities (amp, lt, gt, and quot) as well as + * numeric entities (e.g. #x9f;) with their actual values and + * return the new string. + * + * @access private + * @param string $str The string in which to look for entities + * @return string $new_str The new string entities decoded + */ + function replaceEntities($str) + { + foreach ($this->_entity_replacements as $old => $new) { + $str = preg_replace(sprintf("/&%s;/", $old), $new, $str); + } + + // Replace numeric entities because html_entity_decode doesn't + // do it for us. + $str = preg_replace('~&#x([0-9a-f]+);~ei', 'chr(hexdec("\\1"))', $str); + $str = preg_replace('~&#([0-9]+);~e', 'chr(\\1)', $str); + + return $str; + } + + /** + * Strip single and double quotes off of a string, if they are + * present. + * + * @access private + * @param string $str The original string + * @return string $new_str The new string with leading and + * trailing quotes removed + */ + function removeQuotes($str) + { + $matches = array(); + $double = '/^"(.*)"$/'; + $single = "/^\'(.*)\'$/"; + + if (preg_match($double, $str, $matches)) { + return $matches[1]; + } else if (preg_match($single, $str, $matches)) { + return $matches[1]; + } else { + return $str; + } + } + + /** + * Create a regular expression that will match an opening + * or closing tag from a set of names. + * + * @access private + * @param mixed $tag_names Tag names to match + * @param mixed $close false/0 = no, true/1 = yes, other = maybe + * @param mixed $self_close false/0 = no, true/1 = yes, other = maybe + * @return string $regex A regular expression string to be used + * in, say, preg_match. + */ + function tagPattern($tag_names, $close, $self_close) + { + if (is_array($tag_names)) { + $tag_names = '(?:'.implode('|',$tag_names).')'; + } + if ($close) { + $close = '\/' . (($close == 1)? '' : '?'); + } else { + $close = ''; + } + if ($self_close) { + $self_close = '(?:\/\s*)' . (($self_close == 1)? '' : '?'); + } else { + $self_close = ''; + } + $expr = sprintf($this->_tag_expr, $close, $tag_names, $self_close); + + return sprintf("/%s/%s", $expr, $this->_re_flags); + } + + /** + * Given an HTML document string, this finds all the META tags in + * the document, provided they are found in the + * <HTML><HEAD>...</HEAD> section of the document. The <HTML> tag + * may be missing. + * + * @access private + * @param string $html_string An HTMl document string + * @return array $tag_list Array of tags; each tag is an array of + * attribute -> value. + */ + function getMetaTags($html_string) + { + $html_string = preg_replace($this->_removed_re, + "", + $html_string); + + $key_tags = array($this->tagPattern('html', false, false), + $this->tagPattern('head', false, false), + $this->tagPattern('head', true, false), + $this->tagPattern('html', true, false), + $this->tagPattern(array( + 'body', 'frameset', 'frame', 'p', 'div', + 'table','span','a'), 'maybe', 'maybe')); + $key_tags_pos = array(); + foreach ($key_tags as $pat) { + $matches = array(); + preg_match($pat, $html_string, $matches, PREG_OFFSET_CAPTURE); + if($matches) { + $key_tags_pos[] = $matches[0][1]; + } else { + $key_tags_pos[] = null; + } + } + // no opening head tag + if (is_null($key_tags_pos[1])) { + return array(); + } + // the effective </head> is the min of the following + if (is_null($key_tags_pos[2])) { + $key_tags_pos[2] = strlen($html_string); + } + foreach (array($key_tags_pos[3], $key_tags_pos[4]) as $pos) { + if (!is_null($pos) && $pos < $key_tags_pos[2]) { + $key_tags_pos[2] = $pos; + } + } + // closing head tag comes before opening head tag + if ($key_tags_pos[1] > $key_tags_pos[2]) { + return array(); + } + // if there is an opening html tag, make sure the opening head tag + // comes after it + if (!is_null($key_tags_pos[0]) && $key_tags_pos[1] < $key_tags_pos[0]) { + return array(); + } + $html_string = substr($html_string, $key_tags_pos[1], + ($key_tags_pos[2]-$key_tags_pos[1])); + + $link_data = array(); + $link_matches = array(); + + if (!preg_match_all($this->tagPattern('meta', false, 'maybe'), + $html_string, $link_matches)) { + return array(); + } + + foreach ($link_matches[0] as $link) { + $attr_matches = array(); + preg_match_all($this->_attr_find, $link, $attr_matches); + $link_attrs = array(); + foreach ($attr_matches[0] as $index => $full_match) { + $name = $attr_matches[1][$index]; + $value = $this->replaceEntities( + $this->removeQuotes($attr_matches[2][$index])); + + $link_attrs[strtolower($name)] = $value; + } + $link_data[] = $link_attrs; + } + + return $link_data; + } + + /** + * Looks for a META tag with an "http-equiv" attribute whose value + * is one of ("x-xrds-location", "x-yadis-location"), ignoring + * case. If such a META tag is found, its "content" attribute + * value is returned. + * + * @param string $html_string An HTML document in string format + * @return mixed $content The "content" attribute value of the + * META tag, if found, or null if no such tag was found. + */ + function getHTTPEquiv($html_string) + { + $meta_tags = $this->getMetaTags($html_string); + + if ($meta_tags) { + foreach ($meta_tags as $tag) { + if (array_key_exists('http-equiv', $tag) && + (in_array(strtolower($tag['http-equiv']), + array('x-xrds-location', 'x-yadis-location'))) && + array_key_exists('content', $tag)) { + return $tag['content']; + } + } + } + + return null; + } +} + +?>
\ No newline at end of file diff --git a/Auth/Yadis/PlainHTTPFetcher.php b/Auth/Yadis/PlainHTTPFetcher.php new file mode 100644 index 0000000..3e0ca2b --- /dev/null +++ b/Auth/Yadis/PlainHTTPFetcher.php @@ -0,0 +1,249 @@ +<?php + +/** + * This module contains the plain non-curl HTTP fetcher + * implementation. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Interface import + */ +require_once "Auth/Yadis/HTTPFetcher.php"; + +/** + * This class implements a plain, hand-built socket-based fetcher + * which will be used in the event that CURL is unavailable. + * + * @package OpenID + */ +class Auth_Yadis_PlainHTTPFetcher extends Auth_Yadis_HTTPFetcher { + /** + * Does this fetcher support SSL URLs? + */ + function supportsSSL() + { + return function_exists('openssl_open'); + } + + function get($url, $extra_headers = null) + { + if (!$this->canFetchURL($url)) { + return null; + } + + $redir = true; + + $stop = time() + $this->timeout; + $off = $this->timeout; + + while ($redir && ($off > 0)) { + + $parts = parse_url($url); + + $specify_port = true; + + // Set a default port. + if (!array_key_exists('port', $parts)) { + $specify_port = false; + if ($parts['scheme'] == 'http') { + $parts['port'] = 80; + } elseif ($parts['scheme'] == 'https') { + $parts['port'] = 443; + } else { + return null; + } + } + + if (!array_key_exists('path', $parts)) { + $parts['path'] = '/'; + } + + $host = $parts['host']; + + if ($parts['scheme'] == 'https') { + $host = 'ssl://' . $host; + } + + $user_agent = Auth_OpenID_USER_AGENT; + + $headers = array( + "GET ".$parts['path']. + (array_key_exists('query', $parts) ? + "?".$parts['query'] : ""). + " HTTP/1.0", + "User-Agent: $user_agent", + "Host: ".$parts['host']. + ($specify_port ? ":".$parts['port'] : ""), + "Port: ".$parts['port']); + + $errno = 0; + $errstr = ''; + + if ($extra_headers) { + foreach ($extra_headers as $h) { + $headers[] = $h; + } + } + + @$sock = fsockopen($host, $parts['port'], $errno, $errstr, + $this->timeout); + if ($sock === false) { + return false; + } + + stream_set_timeout($sock, $this->timeout); + + fputs($sock, implode("\r\n", $headers) . "\r\n\r\n"); + + $data = ""; + $kilobytes = 0; + while (!feof($sock) && + $kilobytes < Auth_OpenID_FETCHER_MAX_RESPONSE_KB ) { + $data .= fgets($sock, 1024); + $kilobytes += 1; + } + + fclose($sock); + + // Split response into header and body sections + list($headers, $body) = explode("\r\n\r\n", $data, 2); + $headers = explode("\r\n", $headers); + + $http_code = explode(" ", $headers[0]); + $code = $http_code[1]; + + if (in_array($code, array('301', '302'))) { + $url = $this->_findRedirect($headers); + $redir = true; + } else { + $redir = false; + } + + $off = $stop - time(); + } + + $new_headers = array(); + + foreach ($headers as $header) { + if (preg_match("/:/", $header)) { + $parts = explode(": ", $header, 2); + + if (count($parts) == 2) { + list($name, $value) = $parts; + $new_headers[$name] = $value; + } + } + + } + + return new Auth_Yadis_HTTPResponse($url, $code, $new_headers, $body); + } + + function post($url, $body, $extra_headers = null) + { + if (!$this->canFetchURL($url)) { + return null; + } + + $parts = parse_url($url); + + $headers = array(); + + $post_path = $parts['path']; + if (isset($parts['query'])) { + $post_path .= '?' . $parts['query']; + } + + $headers[] = "POST ".$post_path." HTTP/1.0"; + $headers[] = "Host: " . $parts['host']; + $headers[] = "Content-type: application/x-www-form-urlencoded"; + $headers[] = "Content-length: " . strval(strlen($body)); + + if ($extra_headers && + is_array($extra_headers)) { + $headers = array_merge($headers, $extra_headers); + } + + // Join all headers together. + $all_headers = implode("\r\n", $headers); + + // Add headers, two newlines, and request body. + $request = $all_headers . "\r\n\r\n" . $body; + + // Set a default port. + if (!array_key_exists('port', $parts)) { + if ($parts['scheme'] == 'http') { + $parts['port'] = 80; + } elseif ($parts['scheme'] == 'https') { + $parts['port'] = 443; + } else { + return null; + } + } + + if ($parts['scheme'] == 'https') { + $parts['host'] = sprintf("ssl://%s", $parts['host']); + } + + // Connect to the remote server. + $errno = 0; + $errstr = ''; + + $sock = fsockopen($parts['host'], $parts['port'], $errno, $errstr, + $this->timeout); + + if ($sock === false) { + return null; + } + + stream_set_timeout($sock, $this->timeout); + + // Write the POST request. + fputs($sock, $request); + + // Get the response from the server. + $response = ""; + while (!feof($sock)) { + if ($data = fgets($sock, 128)) { + $response .= $data; + } else { + break; + } + } + + // Split the request into headers and body. + list($headers, $response_body) = explode("\r\n\r\n", $response, 2); + + $headers = explode("\r\n", $headers); + + // Expect the first line of the headers data to be something + // like HTTP/1.1 200 OK. Split the line on spaces and take + // the second token, which should be the return code. + $http_code = explode(" ", $headers[0]); + $code = $http_code[1]; + + $new_headers = array(); + + foreach ($headers as $header) { + if (preg_match("/:/", $header)) { + list($name, $value) = explode(": ", $header, 2); + $new_headers[$name] = $value; + } + + } + + return new Auth_Yadis_HTTPResponse($url, $code, + $new_headers, $response_body); + } +} + +?>
\ No newline at end of file diff --git a/Auth/Yadis/XML.php b/Auth/Yadis/XML.php new file mode 100644 index 0000000..81b2ce2 --- /dev/null +++ b/Auth/Yadis/XML.php @@ -0,0 +1,374 @@ +<?php + +/** + * XML-parsing classes to wrap the domxml and DOM extensions for PHP 4 + * and 5, respectively. + * + * @package OpenID + */ + +/** + * The base class for wrappers for available PHP XML-parsing + * extensions. To work with this Yadis library, subclasses of this + * class MUST implement the API as defined in the remarks for this + * class. Subclasses of Auth_Yadis_XMLParser are used to wrap + * particular PHP XML extensions such as 'domxml'. These are used + * internally by the library depending on the availability of + * supported PHP XML extensions. + * + * @package OpenID + */ +class Auth_Yadis_XMLParser { + /** + * Initialize an instance of Auth_Yadis_XMLParser with some + * XML and namespaces. This SHOULD NOT be overridden by + * subclasses. + * + * @param string $xml_string A string of XML to be parsed. + * @param array $namespace_map An array of ($ns_name => $ns_uri) + * to be registered with the XML parser. May be empty. + * @return boolean $result True if the initialization and + * namespace registration(s) succeeded; false otherwise. + */ + function init($xml_string, $namespace_map) + { + if (!$this->setXML($xml_string)) { + return false; + } + + foreach ($namespace_map as $prefix => $uri) { + if (!$this->registerNamespace($prefix, $uri)) { + return false; + } + } + + return true; + } + + /** + * Register a namespace with the XML parser. This should be + * overridden by subclasses. + * + * @param string $prefix The namespace prefix to appear in XML tag + * names. + * + * @param string $uri The namespace URI to be used to identify the + * namespace in the XML. + * + * @return boolean $result True if the registration succeeded; + * false otherwise. + */ + function registerNamespace($prefix, $uri) + { + // Not implemented. + } + + /** + * Set this parser object's XML payload. This should be + * overridden by subclasses. + * + * @param string $xml_string The XML string to pass to this + * object's XML parser. + * + * @return boolean $result True if the initialization succeeded; + * false otherwise. + */ + function setXML($xml_string) + { + // Not implemented. + } + + /** + * Evaluate an XPath expression and return the resulting node + * list. This should be overridden by subclasses. + * + * @param string $xpath The XPath expression to be evaluated. + * + * @param mixed $node A node object resulting from a previous + * evalXPath call. This node, if specified, provides the context + * for the evaluation of this xpath expression. + * + * @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) + { + // Not implemented. + } + + /** + * Return the textual content of a specified node. + * + * @param mixed $node A node object from a previous call to + * $this->evalXPath(). + * + * @return string $content The content of this node. + */ + function content($node) + { + // Not implemented. + } + + /** + * Return the attributes of a specified node. + * + * @param mixed $node A node object from a previous call to + * $this->evalXPath(). + * + * @return array $attrs An array mapping attribute names to + * values. + */ + function attributes($node) + { + // Not implemented. + } +} + +/** + * This concrete implementation of Auth_Yadis_XMLParser implements + * the appropriate API for the 'domxml' extension which is typically + * packaged with PHP 4. This class will be used whenever the 'domxml' + * extension is detected. See the Auth_Yadis_XMLParser class for + * details on this class's methods. + * + * @package OpenID + */ +class Auth_Yadis_domxml extends Auth_Yadis_XMLParser { + function Auth_Yadis_domxml() + { + $this->xml = null; + $this->doc = null; + $this->xpath = null; + $this->errors = array(); + } + + function setXML($xml_string) + { + $this->xml = $xml_string; + $this->doc = @domxml_open_mem($xml_string, DOMXML_LOAD_PARSING, + $this->errors); + + if (!$this->doc) { + return false; + } + + $this->xpath = $this->doc->xpath_new_context(); + + return true; + } + + function registerNamespace($prefix, $uri) + { + return xpath_register_ns($this->xpath, $prefix, $uri); + } + + function &evalXPath($xpath, $node = null) + { + if ($node) { + $result = @$this->xpath->xpath_eval($xpath, $node); + } else { + $result = @$this->xpath->xpath_eval($xpath); + } + + if (!$result) { + $n = array(); + return $n; + } + + if (!$result->nodeset) { + $n = array(); + return $n; + } + + return $result->nodeset; + } + + function content($node) + { + if ($node) { + return $node->get_content(); + } + } + + function attributes($node) + { + if ($node) { + $arr = $node->attributes(); + $result = array(); + + if ($arr) { + foreach ($arr as $attrnode) { + $result[$attrnode->name] = $attrnode->value; + } + } + + return $result; + } + } +} + +/** + * This concrete implementation of Auth_Yadis_XMLParser implements + * the appropriate API for the 'dom' extension which is typically + * packaged with PHP 5. This class will be used whenever the 'dom' + * extension is detected. See the Auth_Yadis_XMLParser class for + * details on this class's methods. + * + * @package OpenID + */ +class Auth_Yadis_dom extends Auth_Yadis_XMLParser { + function Auth_Yadis_dom() + { + $this->xml = null; + $this->doc = null; + $this->xpath = null; + $this->errors = array(); + } + + function setXML($xml_string) + { + $this->xml = $xml_string; + $this->doc = new DOMDocument; + + if (!$this->doc) { + return false; + } + + if (!@$this->doc->loadXML($xml_string)) { + return false; + } + + $this->xpath = new DOMXPath($this->doc); + + if ($this->xpath) { + return true; + } else { + return false; + } + } + + function registerNamespace($prefix, $uri) + { + return $this->xpath->registerNamespace($prefix, $uri); + } + + function &evalXPath($xpath, $node = null) + { + if ($node) { + $result = @$this->xpath->query($xpath, $node); + } else { + $result = @$this->xpath->query($xpath); + } + + $n = array(); + + if (!$result) { + return $n; + } + + for ($i = 0; $i < $result->length; $i++) { + $n[] = $result->item($i); + } + + return $n; + } + + function content($node) + { + if ($node) { + return $node->textContent; + } + } + + function attributes($node) + { + if ($node) { + $arr = $node->attributes; + $result = array(); + + if ($arr) { + for ($i = 0; $i < $arr->length; $i++) { + $node = $arr->item($i); + $result[$node->nodeName] = $node->nodeValue; + } + } + + return $result; + } + } +} + +global $__Auth_Yadis_defaultParser; +$__Auth_Yadis_defaultParser = null; + +/** + * Set a default parser to override the extension-driven selection of + * available parser classes. This is helpful in a test environment or + * one in which multiple parsers can be used but one is more + * desirable. + * + * @param Auth_Yadis_XMLParser $parser An instance of a + * Auth_Yadis_XMLParser subclass. + */ +function Auth_Yadis_setDefaultParser(&$parser) +{ + global $__Auth_Yadis_defaultParser; + $__Auth_Yadis_defaultParser =& $parser; +} + +function Auth_Yadis_getSupportedExtensions() +{ + return array( + 'dom' => array('classname' => 'Auth_Yadis_dom', + 'libname' => array('dom.so', 'dom.dll')), + 'domxml' => array('classname' => 'Auth_Yadis_domxml', + 'libname' => array('domxml.so', 'php_domxml.dll')), + ); +} + +/** + * Returns an instance of a Auth_Yadis_XMLParser subclass based on + * the availability of PHP extensions for XML parsing. If + * Auth_Yadis_setDefaultParser has been called, the parser used in + * that call will be returned instead. + */ +function &Auth_Yadis_getXMLParser() +{ + global $__Auth_Yadis_defaultParser; + + if (isset($__Auth_Yadis_defaultParser)) { + return $__Auth_Yadis_defaultParser; + } + + $p = null; + $classname = null; + + $extensions = Auth_Yadis_getSupportedExtensions(); + + // Return a wrapper for the resident implementation, if any. + foreach ($extensions as $name => $params) { + if (!extension_loaded($name)) { + foreach ($params['libname'] as $libname) { + if (@dl($libname)) { + $classname = $params['classname']; + } + } + } else { + $classname = $params['classname']; + } + if (isset($classname)) { + $p = new $classname(); + return $p; + } + } + + if (!isset($p)) { + trigger_error('No XML parser was found', E_USER_ERROR); + } else { + Auth_Yadis_setDefaultParser($p); + } + + return $p; +} + +?> diff --git a/Auth/Yadis/XRDS.php b/Auth/Yadis/XRDS.php new file mode 100644 index 0000000..f14a794 --- /dev/null +++ b/Auth/Yadis/XRDS.php @@ -0,0 +1,478 @@ +<?php + +/** + * This module contains the XRDS parsing code. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Require the XPath implementation. + */ +require_once 'Auth/Yadis/XML.php'; + +/** + * This match mode means a given service must match ALL filters passed + * to the Auth_Yadis_XRDS::services() call. + */ +define('SERVICES_YADIS_MATCH_ALL', 101); + +/** + * This match mode means a given service must match ANY filters (at + * least one) passed to the Auth_Yadis_XRDS::services() call. + */ +define('SERVICES_YADIS_MATCH_ANY', 102); + +/** + * The priority value used for service elements with no priority + * specified. + */ +define('SERVICES_YADIS_MAX_PRIORITY', pow(2, 30)); + +/** + * XRD XML namespace + */ +define('Auth_Yadis_XMLNS_XRD_2_0', 'xri://$xrd*($v*2.0)'); + +/** + * XRDS XML namespace + */ +define('Auth_Yadis_XMLNS_XRDS', 'xri://$xrds'); + +function Auth_Yadis_getNSMap() +{ + return array('xrds' => Auth_Yadis_XMLNS_XRDS, + 'xrd' => Auth_Yadis_XMLNS_XRD_2_0); +} + +/** + * @access private + */ +function Auth_Yadis_array_scramble($arr) +{ + $result = array(); + + while (count($arr)) { + $index = array_rand($arr, 1); + $result[] = $arr[$index]; + unset($arr[$index]); + } + + return $result; +} + +/** + * This class represents a <Service> element in an XRDS document. + * Objects of this type are returned by + * Auth_Yadis_XRDS::services() and + * Auth_Yadis_Yadis::services(). Each object corresponds directly + * to a <Service> element in the XRDS and supplies a + * getElements($name) method which you should use to inspect the + * element's contents. See {@link Auth_Yadis_Yadis} for more + * information on the role this class plays in Yadis discovery. + * + * @package OpenID + */ +class Auth_Yadis_Service { + + /** + * Creates an empty service object. + */ + function Auth_Yadis_Service() + { + $this->element = null; + $this->parser = null; + } + + /** + * Return the URIs in the "Type" elements, if any, of this Service + * element. + * + * @return array $type_uris An array of Type URI strings. + */ + function getTypes() + { + $t = array(); + foreach ($this->getElements('xrd:Type') as $elem) { + $c = $this->parser->content($elem); + if ($c) { + $t[] = $c; + } + } + return $t; + } + + function matchTypes($type_uris) + { + $result = array(); + + foreach ($this->getTypes() as $typ) { + if (in_array($typ, $type_uris)) { + $result[] = $typ; + } + } + + return $result; + } + + /** + * Return the URIs in the "URI" elements, if any, of this Service + * element. The URIs are returned sorted in priority order. + * + * @return array $uris An array of URI strings. + */ + function getURIs() + { + $uris = array(); + $last = array(); + + foreach ($this->getElements('xrd:URI') as $elem) { + $uri_string = $this->parser->content($elem); + $attrs = $this->parser->attributes($elem); + if ($attrs && + array_key_exists('priority', $attrs)) { + $priority = intval($attrs['priority']); + if (!array_key_exists($priority, $uris)) { + $uris[$priority] = array(); + } + + $uris[$priority][] = $uri_string; + } else { + $last[] = $uri_string; + } + } + + $keys = array_keys($uris); + sort($keys); + + // Rebuild array of URIs. + $result = array(); + foreach ($keys as $k) { + $new_uris = Auth_Yadis_array_scramble($uris[$k]); + $result = array_merge($result, $new_uris); + } + + $result = array_merge($result, + Auth_Yadis_array_scramble($last)); + + return $result; + } + + /** + * Returns the "priority" attribute value of this <Service> + * element, if the attribute is present. Returns null if not. + * + * @return mixed $result Null or integer, depending on whether + * this Service element has a 'priority' attribute. + */ + function getPriority() + { + $attributes = $this->parser->attributes($this->element); + + if (array_key_exists('priority', $attributes)) { + return intval($attributes['priority']); + } + + return null; + } + + /** + * Used to get XML elements from this object's <Service> element. + * + * This is what you should use to get all custom information out + * of this element. This is used by service filter functions to + * determine whether a service element contains specific tags, + * etc. NOTE: this only considers elements which are direct + * children of the <Service> element for this object. + * + * @param string $name The name of the element to look for + * @return array $list An array of elements with the specified + * name which are direct children of the <Service> element. The + * nodes returned by this function can be passed to $this->parser + * methods (see {@link Auth_Yadis_XMLParser}). + */ + function getElements($name) + { + return $this->parser->evalXPath($name, $this->element); + } +} + +/* + * 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. + * + * You should not instantiate this class directly; rather, call + * parseXRDS statically: + * + * <pre> $xrds = Auth_Yadis_XRDS::parseXRDS($xml_string);</pre> + * + * If the XRDS can be parsed and is valid, an instance of + * Auth_Yadis_XRDS will be returned. Otherwise, null will be + * returned. This class is used by the Auth_Yadis_Yadis::discover + * method. + * + * @package OpenID + */ +class Auth_Yadis_XRDS { + + /** + * Instantiate a Auth_Yadis_XRDS object. Requires an XPath + * instance which has been used to parse a valid XRDS document. + */ + function Auth_Yadis_XRDS(&$xmlParser, &$xrdNodes) + { + $this->parser =& $xmlParser; + $this->xrdNode = $xrdNodes[count($xrdNodes) - 1]; + $this->allXrdNodes =& $xrdNodes; + $this->serviceList = array(); + $this->_parse(); + } + + /** + * Parse an XML string (XRDS document) and return either a + * Auth_Yadis_XRDS object or null, depending on whether the + * XRDS XML is valid. + * + * @param string $xml_string An XRDS XML string. + * @return mixed $xrds An instance of Auth_Yadis_XRDS or null, + * depending on the validity of $xml_string + */ + function &parseXRDS($xml_string, $extra_ns_map = null) + { + $_null = null; + + if (!$xml_string) { + return $_null; + } + + $parser = Auth_Yadis_getXMLParser(); + + $ns_map = Auth_Yadis_getNSMap(); + + if ($extra_ns_map && is_array($extra_ns_map)) { + $ns_map = array_merge($ns_map, $extra_ns_map); + } + + if (!($parser && $parser->init($xml_string, $ns_map))) { + return $_null; + } + + // Try to get root element. + $root = $parser->evalXPath('/xrds:XRDS[1]'); + if (!$root) { + return $_null; + } + + if (is_array($root)) { + $root = $root[0]; + } + + $attrs = $parser->attributes($root); + + if (array_key_exists('xmlns:xrd', $attrs) && + $attrs['xmlns:xrd'] != Auth_Yadis_XMLNS_XRDS) { + return $_null; + } else if (array_key_exists('xmlns', $attrs) && + preg_match('/xri/', $attrs['xmlns']) && + $attrs['xmlns'] != Auth_Yadis_XMLNS_XRD_2_0) { + return $_null; + } + + // Get the last XRD node. + $xrd_nodes = $parser->evalXPath('/xrds:XRDS[1]/xrd:XRD'); + + if (!$xrd_nodes) { + return $_null; + } + + $xrds = new Auth_Yadis_XRDS($parser, $xrd_nodes); + return $xrds; + } + + /** + * @access private + */ + function _addService($priority, $service) + { + $priority = intval($priority); + + if (!array_key_exists($priority, $this->serviceList)) { + $this->serviceList[$priority] = array(); + } + + $this->serviceList[$priority][] = $service; + } + + /** + * Creates the service list using nodes from the XRDS XML + * document. + * + * @access private + */ + function _parse() + { + $this->serviceList = array(); + + $services = $this->parser->evalXPath('xrd:Service', $this->xrdNode); + + foreach ($services as $node) { + $s =& new Auth_Yadis_Service(); + $s->element = $node; + $s->parser =& $this->parser; + + $priority = $s->getPriority(); + + if ($priority === null) { + $priority = SERVICES_YADIS_MAX_PRIORITY; + } + + $this->_addService($priority, $s); + } + } + + /** + * Returns a list of service objects which correspond to <Service> + * elements in the XRDS XML document for this object. + * + * Optionally, an array of filter callbacks may be given to limit + * the list of returned service objects. Furthermore, the default + * mode is to return all service objects which match ANY of the + * specified filters, but $filter_mode may be + * SERVICES_YADIS_MATCH_ALL if you want to be sure that the + * returned services match all the given filters. See {@link + * Auth_Yadis_Yadis} for detailed usage information on filter + * functions. + * + * @param mixed $filters An array of callbacks to filter the + * returned services, or null if all services are to be returned. + * @param integer $filter_mode SERVICES_YADIS_MATCH_ALL or + * SERVICES_YADIS_MATCH_ANY, depending on whether the returned + * services should match ALL or ANY of the specified filters, + * respectively. + * @return mixed $services An array of {@link + * Auth_Yadis_Service} objects if $filter_mode is a valid + * mode; null if $filter_mode is an invalid mode (i.e., not + * SERVICES_YADIS_MATCH_ANY or SERVICES_YADIS_MATCH_ALL). + */ + function services($filters = null, + $filter_mode = SERVICES_YADIS_MATCH_ANY) + { + + $pri_keys = array_keys($this->serviceList); + sort($pri_keys, SORT_NUMERIC); + + // If no filters are specified, return the entire service + // list, ordered by priority. + if (!$filters || + (!is_array($filters))) { + + $result = array(); + foreach ($pri_keys as $pri) { + $result = array_merge($result, $this->serviceList[$pri]); + } + + return $result; + } + + // If a bad filter mode is specified, return null. + if (!in_array($filter_mode, array(SERVICES_YADIS_MATCH_ANY, + SERVICES_YADIS_MATCH_ALL))) { + return null; + } + + // Otherwise, use the callbacks in the filter list to + // determine which services are returned. + $filtered = array(); + + foreach ($pri_keys as $priority_value) { + $service_obj_list = $this->serviceList[$priority_value]; + + foreach ($service_obj_list as $service) { + + $matches = 0; + + foreach ($filters as $filter) { + if (call_user_func_array($filter, array($service))) { + $matches++; + + if ($filter_mode == SERVICES_YADIS_MATCH_ANY) { + $pri = $service->getPriority(); + if ($pri === null) { + $pri = SERVICES_YADIS_MAX_PRIORITY; + } + + if (!array_key_exists($pri, $filtered)) { + $filtered[$pri] = array(); + } + + $filtered[$pri][] = $service; + break; + } + } + } + + if (($filter_mode == SERVICES_YADIS_MATCH_ALL) && + ($matches == count($filters))) { + + $pri = $service->getPriority(); + if ($pri === null) { + $pri = SERVICES_YADIS_MAX_PRIORITY; + } + + if (!array_key_exists($pri, $filtered)) { + $filtered[$pri] = array(); + } + $filtered[$pri][] = $service; + } + } + } + + $pri_keys = array_keys($filtered); + sort($pri_keys, SORT_NUMERIC); + + $result = array(); + foreach ($pri_keys as $pri) { + $result = array_merge($result, $filtered[$pri]); + } + + return $result; + } +} + +?>
\ No newline at end of file diff --git a/Auth/Yadis/XRI.php b/Auth/Yadis/XRI.php new file mode 100644 index 0000000..4e34623 --- /dev/null +++ b/Auth/Yadis/XRI.php @@ -0,0 +1,234 @@ +<?php + +/** + * Routines for XRI resolution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'Auth/Yadis/Misc.php'; +require_once 'Auth/Yadis/Yadis.php'; +require_once 'Auth/OpenID.php'; + +function Auth_Yadis_getDefaultProxy() +{ + return 'http://xri.net/'; +} + +function Auth_Yadis_getXRIAuthorities() +{ + return array('!', '=', '@', '+', '$', '('); +} + +function Auth_Yadis_getEscapeRE() +{ + $parts = array(); + foreach (array_merge(Auth_Yadis_getUCSChars(), + Auth_Yadis_getIPrivateChars()) as $pair) { + list($m, $n) = $pair; + $parts[] = sprintf("%s-%s", chr($m), chr($n)); + } + + return sprintf('/[%s]/', implode('', $parts)); +} + +function Auth_Yadis_getXrefRE() +{ + return '/\((.*?)\)/'; +} + +function Auth_Yadis_identifierScheme($identifier) +{ + if (Auth_Yadis_startswith($identifier, 'xri://') || + ($identifier && + in_array($identifier[0], Auth_Yadis_getXRIAuthorities()))) { + return "XRI"; + } else { + return "URI"; + } +} + +function Auth_Yadis_toIRINormal($xri) +{ + if (!Auth_Yadis_startswith($xri, 'xri://')) { + $xri = 'xri://' . $xri; + } + + return Auth_Yadis_escapeForIRI($xri); +} + +function _escape_xref($xref_match) +{ + $xref = $xref_match[0]; + $xref = str_replace('/', '%2F', $xref); + $xref = str_replace('?', '%3F', $xref); + $xref = str_replace('#', '%23', $xref); + return $xref; +} + +function Auth_Yadis_escapeForIRI($xri) +{ + $xri = str_replace('%', '%25', $xri); + $xri = preg_replace_callback(Auth_Yadis_getXrefRE(), + '_escape_xref', $xri); + return $xri; +} + +function Auth_Yadis_toURINormal($xri) +{ + return Auth_Yadis_iriToURI(Auth_Yadis_toIRINormal($xri)); +} + +function Auth_Yadis_iriToURI($iri) +{ + if (1) { + return $iri; + } else { + // According to RFC 3987, section 3.1, "Mapping of IRIs to URIs" + return preg_replace_callback(Auth_Yadis_getEscapeRE(), + 'Auth_Yadis_pct_escape_unicode', $iri); + } +} + + +function Auth_Yadis_XRIAppendArgs($url, $args) +{ + // Append some arguments to an HTTP query. Yes, this is just like + // OpenID's appendArgs, but with special seasoning for XRI + // queries. + + if (count($args) == 0) { + return $url; + } + + // Non-empty array; if it is an array of arrays, use multisort; + // otherwise use sort. + if (array_key_exists(0, $args) && + is_array($args[0])) { + // Do nothing here. + } else { + $keys = array_keys($args); + sort($keys); + $new_args = array(); + foreach ($keys as $key) { + $new_args[] = array($key, $args[$key]); + } + $args = $new_args; + } + + // According to XRI Resolution section "QXRI query parameters": + // + // "If the original QXRI had a null query component (only a + // leading question mark), or a query component consisting of + // only question marks, one additional leading question mark MUST + // be added when adding any XRI resolution parameters." + if (strpos(rtrim($url, '?'), '?') !== false) { + $sep = '&'; + } else { + $sep = '?'; + } + + return $url . $sep . Auth_OpenID::httpBuildQuery($args); +} + +function Auth_Yadis_providerIsAuthoritative($providerID, $canonicalID) +{ + $lastbang = strrpos($canonicalID, '!'); + $p = substr($canonicalID, 0, $lastbang); + return $p == $providerID; +} + +function Auth_Yadis_rootAuthority($xri) +{ + // Return the root authority for an XRI. + + $root = null; + + if (Auth_Yadis_startswith($xri, 'xri://')) { + $xri = substr($xri, 6); + } + + $authority = explode('/', $xri, 2); + $authority = $authority[0]; + if ($authority[0] == '(') { + // Cross-reference. + // XXX: This is incorrect if someone nests cross-references so + // there is another close-paren in there. Hopefully nobody + // does that before we have a real xriparse function. + // Hopefully nobody does that *ever*. + $root = substr($authority, 0, strpos($authority, ')') + 1); + } else if (in_array($authority[0], Auth_Yadis_getXRIAuthorities())) { + // Other XRI reference. + $root = $authority[0]; + } else { + // IRI reference. + $_segments = explode("!", $authority); + $segments = array(); + foreach ($_segments as $s) { + $segments = array_merge($segments, explode("*", $s)); + } + $root = $segments[0]; + } + + return Auth_Yadis_XRI($root); +} + +function Auth_Yadis_XRI($xri) +{ + if (!Auth_Yadis_startswith($xri, 'xri://')) { + $xri = 'xri://' . $xri; + } + return $xri; +} + +function Auth_Yadis_getCanonicalID($iname, $xrds) +{ + // Returns false or a canonical ID value. + + // Now nodes are in reverse order. + $xrd_list = array_reverse($xrds->allXrdNodes); + $parser =& $xrds->parser; + $node = $xrd_list[0]; + + $canonicalID_nodes = $parser->evalXPath('xrd:CanonicalID', $node); + + if (!$canonicalID_nodes) { + return false; + } + + $canonicalID = $canonicalID_nodes[0]; + $canonicalID = Auth_Yadis_XRI($parser->content($canonicalID)); + + $childID = $canonicalID; + + for ($i = 1; $i < count($xrd_list); $i++) { + $xrd = $xrd_list[$i]; + + $parent_sought = substr($childID, 0, strrpos($childID, '!')); + $parentCID = $parser->evalXPath('xrd:CanonicalID', $xrd); + if (!$parentCID) { + return false; + } + $parentCID = Auth_Yadis_XRI($parser->content($parentCID[0])); + + if (strcasecmp($parent_sought, $parentCID)) { + // raise XRDSFraud. + return false; + } + + $childID = $parent_sought; + } + + $root = Auth_Yadis_rootAuthority($iname); + if (!Auth_Yadis_providerIsAuthoritative($root, $childID)) { + // raise XRDSFraud. + return false; + } + + return $canonicalID; +} + +?> diff --git a/Auth/Yadis/XRIRes.php b/Auth/Yadis/XRIRes.php new file mode 100644 index 0000000..4e8e8d0 --- /dev/null +++ b/Auth/Yadis/XRIRes.php @@ -0,0 +1,72 @@ +<?php + +/** + * Code for using a proxy XRI resolver. + */ + +require_once 'Auth/Yadis/XRDS.php'; +require_once 'Auth/Yadis/XRI.php'; + +class Auth_Yadis_ProxyResolver { + function Auth_Yadis_ProxyResolver(&$fetcher, $proxy_url = null) + { + $this->fetcher =& $fetcher; + $this->proxy_url = $proxy_url; + if (!$this->proxy_url) { + $this->proxy_url = Auth_Yadis_getDefaultProxy(); + } + } + + function queryURL($xri, $service_type = null) + { + // trim off the xri:// prefix + $qxri = substr(Auth_Yadis_toURINormal($xri), 6); + $hxri = $this->proxy_url . $qxri; + $args = array( + '_xrd_r' => 'application/xrds+xml' + ); + + if ($service_type) { + $args['_xrd_t'] = $service_type; + } else { + // Don't perform service endpoint selection. + $args['_xrd_r'] .= ';sep=false'; + } + + $query = Auth_Yadis_XRIAppendArgs($hxri, $args); + return $query; + } + + function query($xri, $service_types, $filters = array()) + { + $services = array(); + $canonicalID = null; + foreach ($service_types as $service_type) { + $url = $this->queryURL($xri, $service_type); + $response = $this->fetcher->get($url); + if ($response->status != 200 and $response->status != 206) { + continue; + } + $xrds = Auth_Yadis_XRDS::parseXRDS($response->body); + if (!$xrds) { + continue; + } + $canonicalID = Auth_Yadis_getCanonicalID($xri, + $xrds); + + if ($canonicalID === false) { + return null; + } + + $some_services = $xrds->services($filters); + $services = array_merge($services, $some_services); + // TODO: + // * If we do get hits for multiple service_types, we're + // almost certainly going to have duplicated service + // entries and broken priority ordering. + } + return array($canonicalID, $services); + } +} + +?> diff --git a/Auth/Yadis/Yadis.php b/Auth/Yadis/Yadis.php new file mode 100644 index 0000000..d89f77c --- /dev/null +++ b/Auth/Yadis/Yadis.php @@ -0,0 +1,382 @@ +<?php + +/** + * The core PHP Yadis implementation. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Need both fetcher types so we can use the right one based on the + * presence or absence of CURL. + */ +require_once "Auth/Yadis/PlainHTTPFetcher.php"; +require_once "Auth/Yadis/ParanoidHTTPFetcher.php"; + +/** + * Need this for parsing HTML (looking for META tags). + */ +require_once "Auth/Yadis/ParseHTML.php"; + +/** + * Need this to parse the XRDS document during Yadis discovery. + */ +require_once "Auth/Yadis/XRDS.php"; + +/** + * XRDS (yadis) content type + */ +define('Auth_Yadis_CONTENT_TYPE', 'application/xrds+xml'); + +/** + * Yadis header + */ +define('Auth_Yadis_HEADER_NAME', 'X-XRDS-Location'); + +/** + * Contains the result of performing Yadis discovery on a URI. + * + * @package OpenID + */ +class Auth_Yadis_DiscoveryResult { + + // The URI that was passed to the fetcher + var $request_uri = null; + + // The result of following redirects from the request_uri + var $normalized_uri = null; + + // The URI from which the response text was returned (set to + // None if there was no XRDS document found) + var $xrds_uri = null; + + var $xrds = null; + + // The content-type returned with the response_text + var $content_type = null; + + // The document returned from the xrds_uri + var $response_text = null; + + // Did the discovery fail miserably? + var $failed = false; + + function Auth_Yadis_DiscoveryResult($request_uri) + { + // Initialize the state of the object + // sets all attributes to None except the request_uri + $this->request_uri = $request_uri; + } + + function fail() + { + $this->failed = true; + } + + function isFailure() + { + return $this->failed; + } + + /** + * Returns the list of service objects as described by the XRDS + * document, if this yadis object represents a successful Yadis + * discovery. + * + * @return array $services An array of {@link Auth_Yadis_Service} + * objects + */ + function services() + { + if ($this->xrds) { + return $this->xrds->services(); + } + + return null; + } + + function usedYadisLocation() + { + // Was the Yadis protocol's indirection used? + return $this->normalized_uri != $this->xrds_uri; + } + + function isXRDS() + { + // Is the response text supposed to be an XRDS document? + return ($this->usedYadisLocation() || + $this->content_type == Auth_Yadis_CONTENT_TYPE); + } +} + +/** + * + * 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 + * performs the discovery AND stores the result of the discovery. + * + * First, require this library into your program source: + * + * <pre> require_once "Auth/Yadis/Yadis.php";</pre> + * + * To perform Yadis discovery, first call the "discover" method + * statically with a URI parameter: + * + * <pre> $http_response = array(); + * $fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); + * $yadis_object = Auth_Yadis_Yadis::discover($uri, + * $http_response, $fetcher);</pre> + * + * If the discovery succeeds, $yadis_object will be an instance of + * {@link Auth_Yadis_Yadis}. If not, it will be null. The XRDS + * document found during discovery should have service descriptions, + * which can be accessed by calling + * + * <pre> $service_list = $yadis_object->services();</pre> + * + * which returns an array of objects which describe each service. + * These objects are instances of Auth_Yadis_Service. Each object + * describes exactly one whole Service element, complete with all of + * its Types and URIs (no expansion is performed). The common use + * case for using the service objects returned by services() is to + * write one or more filter functions and pass those to services(): + * + * <pre> $service_list = $yadis_object->services( + * array("filterByURI", + * "filterByExtension"));</pre> + * + * The filter functions (whose names appear in the array passed to + * services()) take the following form: + * + * <pre> function myFilter(&$service) { + * // Query $service object here. Return true if the service + * // matches your query; false if not. + * }</pre> + * + * This is an example of a filter which uses a regular expression to + * match the content of URI tags (note that the Auth_Yadis_Service + * class provides a getURIs() method which you should use instead of + * this contrived example): + * + * <pre> + * function URIMatcher(&$service) { + * foreach ($service->getElements('xrd:URI') as $uri) { + * if (preg_match("/some_pattern/", + * $service->parser->content($uri))) { + * return true; + * } + * } + * return false; + * }</pre> + * + * The filter functions you pass will be called for each service + * object to determine which ones match the criteria your filters + * specify. The default behavior is that if a given service object + * matches ANY of the filters specified in the services() call, it + * will be returned. You can specify that a given service object will + * be returned ONLY if it matches ALL specified filters by changing + * the match mode of services(): + * + * <pre> $yadis_object->services(array("filter1", "filter2"), + * SERVICES_YADIS_MATCH_ALL);</pre> + * + * See {@link SERVICES_YADIS_MATCH_ALL} and {@link + * SERVICES_YADIS_MATCH_ANY}. + * + * Services described in an XRDS should have a library which you'll + * probably be using. Those libraries are responsible for defining + * filters that can be used with the "services()" call. If you need + * to write your own filter, see the documentation for {@link + * Auth_Yadis_Service}. + * + * @package OpenID + */ +class Auth_Yadis_Yadis { + + /** + * Returns an HTTP fetcher object. If the CURL extension is + * present, an instance of {@link Auth_Yadis_ParanoidHTTPFetcher} + * is returned. If not, an instance of + * {@link Auth_Yadis_PlainHTTPFetcher} is returned. + * + * If Auth_Yadis_CURL_OVERRIDE is defined, this method will always + * return a {@link Auth_Yadis_PlainHTTPFetcher}. + */ + function getHTTPFetcher($timeout = 20) + { + if (Auth_Yadis_Yadis::curlPresent() && + (!defined('Auth_Yadis_CURL_OVERRIDE'))) { + $fetcher = new Auth_Yadis_ParanoidHTTPFetcher($timeout); + } else { + $fetcher = new Auth_Yadis_PlainHTTPFetcher($timeout); + } + return $fetcher; + } + + function curlPresent() + { + return function_exists('curl_init'); + } + + /** + * @access private + */ + function _getHeader($header_list, $names) + { + foreach ($header_list as $name => $value) { + foreach ($names as $n) { + if (strtolower($name) == strtolower($n)) { + return $value; + } + } + } + + return null; + } + + /** + * @access private + */ + function _getContentType($content_type_header) + { + if ($content_type_header) { + $parts = explode(";", $content_type_header); + return strtolower($parts[0]); + } + } + + /** + * This should be called statically and will build a Yadis + * instance if the discovery process succeeds. This implements + * Yadis discovery as specified in the Yadis specification. + * + * @param string $uri The URI on which to perform Yadis discovery. + * + * @param array $http_response An array reference where the HTTP + * response object will be stored (see {@link + * Auth_Yadis_HTTPResponse}. + * + * @param Auth_Yadis_HTTPFetcher $fetcher An instance of a + * Auth_Yadis_HTTPFetcher subclass. + * + * @param array $extra_ns_map An array which maps namespace names + * to namespace URIs to be used when parsing the Yadis XRDS + * document. + * + * @param integer $timeout An optional fetcher timeout, in seconds. + * + * @return mixed $obj Either null or an instance of + * Auth_Yadis_Yadis, depending on whether the discovery + * succeeded. + */ + function discover($uri, &$fetcher, + $extra_ns_map = null, $timeout = 20) + { + $result = new Auth_Yadis_DiscoveryResult($uri); + + $request_uri = $uri; + $headers = array("Accept: " . Auth_Yadis_CONTENT_TYPE . + ', text/html; q=0.3, application/xhtml+xml; q=0.5'); + + if ($fetcher === null) { + $fetcher = Auth_Yadis_Yadis::getHTTPFetcher($timeout); + } + + $response = $fetcher->get($uri, $headers); + + if (!$response || ($response->status != 200 and + $response->status != 206)) { + $result->fail(); + return $result; + } + + $result->normalized_uri = $response->final_url; + $result->content_type = Auth_Yadis_Yadis::_getHeader( + $response->headers, + array('content-type')); + + if ($result->content_type && + (Auth_Yadis_Yadis::_getContentType($result->content_type) == + Auth_Yadis_CONTENT_TYPE)) { + $result->xrds_uri = $result->normalized_uri; + } else { + $yadis_location = Auth_Yadis_Yadis::_getHeader( + $response->headers, + array(Auth_Yadis_HEADER_NAME)); + + if (!$yadis_location) { + $parser = new Auth_Yadis_ParseHTML(); + $yadis_location = $parser->getHTTPEquiv($response->body); + } + + if ($yadis_location) { + $result->xrds_uri = $yadis_location; + + $response = $fetcher->get($yadis_location); + + if ((!$response) || ($response->status != 200 and + $response->status != 206)) { + $result->fail(); + return $result; + } + + $result->content_type = Auth_Yadis_Yadis::_getHeader( + $response->headers, + array('content-type')); + } + } + + $result->response_text = $response->body; + return $result; + } +} + +?> diff --git a/CHANGES-2.1.0 b/CHANGES-2.1.0 new file mode 100644 index 0000000..5e68fb9 --- /dev/null +++ b/CHANGES-2.1.0 @@ -0,0 +1,51 @@ +* API Changes + * AX::FetchResponse::fromSuccessResponse - return null when AX + response arguments are absent + * Alter AX fromOpenIDRequest() to take Auth_OpenID_AuthRequest + object instead of Auth_OpenID_Message object so that it matches + its counterpart methods in SREG and PAPE extensions. + * PAPE (Provider Authentication Policy Extension) module + * Updated extension for specification draft 2 + * Auth_OpenID_PAPE_Request::fromSuccessResponse returns None if + PAPE response arguments were not signed + * Added functions to generate request/response HTML forms with + auto-submission javascript + * Consumer (relying party) API: + Auth_OpenID_AuthRequest::htmlMarkup + * Server API: Auth_OpenID_OpenIDResponse::toHTML + +* New Features + * Added examples/discover.php, an OpenID service discovery tool + * Add optional form_tag_attrs argument to + Auth_OpenID_ServerResponse::toFormMarkup for setting arbitrary + FORM element attributes + * Fetchers now only read/request first megabyte of response + +* Bug Fixes + * NOT NULL constraints were added to SQLStore tables where + appropriate + * Yadis discovery now properly falls back to HTML-based discovery if + it fails to get an XRDS document + * Auth_OpenID_Decoder now behaves correctly when given a protocol + message with an invalid OpenID namespace or a missing OpenID mode + * Auth_OpenID_OpenIDResponse::toFormMarkup: Use return_to from the + request, not the response fields (Not all responses (i.e. cancel, + setup_needed) include a return_to field.) + * normalize return_to URL before performing return_to verification + * Auth_OpenID_Consumer::_verifyDiscoveryResults: fall back to OpenID + 1.0 type if 1.1 endpoint cannot be found + * Auth_Yadis_ParanoidHTTPFetcher now works correctly with both array + and non-array CURL versions + * Clarified licensing language in all source files + * OpenID 1 association requests no longer explicitly set + no-encryption session type + * Auth_OpenID_ServiceEndpoint::getDisplayIdentifier no longer + includes a fragment, if present, in display identifiers + * check_authentication requests: copy entire response, not just + signed fields. Fixes missing namespace in check_authentication + requests + * Yadis discovery now includes application/xhtml+xml and qualities + in the Accept header + * Normalize URLs correctly with URINorm.php + * Auth_OpenID_MySQLStore: Use ENGINE instead of TYPE when creating + tables @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. @@ -0,0 +1,85 @@ +What's New in PHP OpenID 2.0 +============================ + +This library implements both the OpenID 1 and OpenID 2 protocols. The +API changes in this version of the library are minimal and mostly +backwards-compatible with current RP and Server implementations. If +you're already using this library for OpenID 1, only a few small +changes (see Summary below) will be needed once you upgrade. + +The big news here is compatibility with version 2.0 of the OpenID +protocol. Highlights include: + + * Simple Registration support in a new module Auth/OpenID/SReg.php. + (Those previously using + Auth_OpenID_SuccessResponse::extensionResponse() are advised to + look here and at the example consumer and server for detailed usage + information.) + * OpenID provider-driven identifier selection. + * "Negotiators" allow you to define which association types to use. + * Improved examples/detect.php script (bugs fixed) + * Improved layout of example consumer (see examples/consumer) + * An improved HTML parser implementation + * Library is now immune to being included inside functions and + methods + * Fixes to avoid multibyte overloading problems + +If you've written your own custom store or code that interacts +directly with it, you'll need to review the change notes for +Auth_OpenID_Interface in Auth/OpenID/Interface.php. + + +Upgrading from earlier versions of this library +----------------------------------------------- + +One of the additions to the OpenID protocol was a specified nonce +format for one-way nonces. As a result, the nonce table in the +SQL-driven stores has changed. You'll need to run the Python script +contrib/upgrade-store-1.1-to-2.0 to upgrade your store, or you'll +encounter errors about the wrong number of columns in the oid_nonces +table. To run the script, you'll need a python module supporting your +database type: pysqlite2, psycopg, or MySQLdb. + +If you cannot run the Python script, you can re-create your store by +dropping the tables in the store and calling createTables() on the +store object. + +Consumers should now pass the consumer return_to URL to +Auth_OpenID_Consumer::complete() to defend against return_to URL +tampering. This has REPLACED the old parameter, $query. $query is +now a second optional parameter. It is STRONGLY RECOMMENDED that you +never override $query, since the OpenID library uses its own logic to +sidestep PHP's broken request-processing code. + + +Summary of API Changes +---------------------- + + - Auth_OpenID::fixArgs is now no longer necessary, and +Auth_OpenID_Consumer::complete and Auth_OpenID_Server::decodeRequest +no longer take query argument arrays. *You should no longer pass any +parameters to these methods.* + + - Auth_OpenID_SuccessResponse::extensionResponse() is no longer the +preferred way to extract extension response parameters from the OpenID +response. Instead, see the Auth/OpenID/SReg.php module and the +example consumer and server for detailed usage information on +constructing Simple Registration requests and inspecting responses. +extensionResponse() is still valid, but now takes a second parameter +(bool) indicating whether extension args should be signed. + + - The Auth_OpenID_Server's response answer() method now takes +additional parameters to support provider-driven identifier selection. +See the example server and the documentation for +Auth_OpenID_CheckIDRequest::answer. + + - Auth_OpenID_Consumer::complete() now takes two args: + + - $return_to, a required string that is the return URL passed to + Auth_OpenID_AuthRequest::redirectURL() + + - $query, an optional array (or null if absent) denoting the query + parameters of the OpenID response. If null, the response data + will be extracted from the PHP request environment. Library + users SHOULD NOT ever pass anything for $query unless they're + testing the library.
\ No newline at end of file @@ -0,0 +1,139 @@ + +PHP OpenID +---------- + +This is the PHP OpenID library by JanRain, Inc. You can visit our +website for more information about this package and other OpenID +implementations and tools: + + http://www.openidenabled.com/ + +GETTING STARTED +=============== + +First, run the 'examples/detect.php' script either from the command +line or via the web. It will generate a report of any system +configuration changes necessary to run the library. + +INSTALLATION +============ + +You will need PHP 4.3.0 or greater to use this library. We have +tested the library Linux on PHP 4.3.0, 4.4.1, 5.0.5, and 5.1.1. We +have tested the library on Windows XP on PHP 4.4.4. + +Follow these steps: + +1. Install dependencies. + + - Enable either the GMP extension or Bcmath extension. (GMP is + STRONGLY recommended because it's MUCH faster!) This is + required. + + - Enable the CURL extension. + + - If you plan to use SQLite, PostgreSQL, or MySQL to store OpenID + data, you'll need PEAR DB. You can install this by running this + as root: + + # pear install DB + + You'll also need to install and enable the appropriate PHP + database extension. Alternatively, you can store OpenID data on + the filesystem instead of using a relational database. Nothing + special is required for using the filesystem method. + + - Install either the DOM or domxml PHP XML processing extension, + but not both (they are incompatible). + +2. Copy the Auth/ directory into your PHP include path. + +TESTING YOUR SETUP +================== + +You can use the example code to test your setup. To run the example +consumer or server, follow the instructions in the examples/README +file. + +USING THE API +============= + +The best way to get started using the API is to take a look at the +example consumer and server in the examples/ directory. See the +examples/README file for more details. + +TROUBLESHOOTING +=============== + +* If you're unable to use an OpenID URL with the library, you may want +to try using the discover tool (examples/discover.php). This tool +will perform OpenID discovery on the identifier and give a list of +discovered OpenID services and their types. + +* On some systems, PHP basedir restrictions prevent web servers from +opening a source of randomness, such as /dev/urandom. If your PHP +OpenID library has trouble getting a satisfactory source of +randomness, check your Apache and PHP configurations to be sure that +the randomness source is in the list of allowed paths for the +"open_basedir" option. + +* In some cases, bugs in the GMP math library will result in signature +validation errors when using this library. Since GMP is preferred +over bcmath (for performance), you will have to define +Auth_OpenID_BUGGY_GMP in your application *before* importing any of +the library code: + + define('Auth_OpenID_BUGGY_GMP', true); + +* Not all PHP installations support SSL. You can find out if yours +supports SSL by reading the "HTTP Fetching" section of the output of +"examples/detect.php." If your installation does not support SSL, +then https:// identity URLs and server URLs will not be supported by +the library. An attempt to use such an identity URL will be +equivalent to using an invalid OpenID. To enable SSL support, +recompile PHP with OpenSSL support or install the appropriate OpenSSL +module for your platform. If you are using CURL, CURL will need to be +built with OpenSSL support. + +GETTING HELP +============ + +If you have any questions, recommendations, or patches, please tell +us! Subscribe to our OpenID development discussion list at + + http://lists.openidenabled.com/mailman/listinfo/dev + +DOCUMENTATION +============= + +You can view the HTML library documentation in the doc/ directory. +You can also find it on the web at + + http://www.openidenabled.com/resources/docs/openid/php/ + +This package's documentation is in PhpDoc format. To generate the +documentation, install phpdoc and run the admin/makedoc.sh script. +Phpdoc lives at: + + http://www.phpdoc.org/ + +CONTRIBUTING +============ + +If you have a bugfix or feature you'd like to contribute, don't +hesitate to send it to us. Post your patch to the development list at + + http://lists.openidenabled.com/mailman/listinfo/dev + +For more detailed information on how to contribute, see + + http://openidenabled.com/contribute/ + +To run the test suite included with this package, install PHPUnit 1.x +and run + + php admin/texttest.php + +PHPUnit 1.x can be found at + + http://pear.phpunit.de/get/ diff --git a/README.Debian b/README.Debian new file mode 100644 index 0000000..1928268 --- /dev/null +++ b/README.Debian @@ -0,0 +1,8 @@ +Development Environment Setup +============================= + +Janrain note: You'll need to run these commands to generate +documentation for this project: + + apt-get install php4-pear + pear install PhpDocumentor diff --git a/Tests/Auth/OpenID/AX.php b/Tests/Auth/OpenID/AX.php new file mode 100644 index 0000000..1b46945 --- /dev/null +++ b/Tests/Auth/OpenID/AX.php @@ -0,0 +1,794 @@ +<?php + +/* + * Tests for the attribute exchange extension module + */ + +require_once "PHPUnit.php"; +require_once "Auth/OpenID/AX.php"; +require_once "Auth/OpenID/Message.php"; +require_once "Auth/OpenID/Consumer.php"; +require_once "Auth/OpenID/Server.php"; + +class BogusAXMessage extends Auth_OpenID_AX_Message { + var $mode = 'bogus'; + + function getExtensionArgs() + { + return $this->_newArgs(); + } +} + +class AXMessageTest extends PHPUnit_TestCase { + function setUp() + { + $this->bax = new BogusAXMessage(); + } + + function test_checkMode() + { + $result = $this->bax->_checkMode(array()); + $this->assertTrue(Auth_OpenID_AX::isError($result)); + + $result = $this->bax->_checkMode(array('mode' => 'fetch_request')); + $this->assertTrue(Auth_OpenID_AX::isError($result)); + + // does not raise an exception when the mode is right + $result = $this->bax->_checkMode(array('mode' => $this->bax->mode)); + $this->assertTrue($result === true); + } + + /* + * _newArgs generates something that has the correct mode + */ + function test_checkMode_newArgs() + { + $result = $this->bax->_checkMode($this->bax->_newArgs()); + $this->assertTrue($result === true); + } +} + +class AttrInfoTest extends PHPUnit_TestCase { + function test_construct() + { + $type_uri = 'a uri'; + $ainfo = Auth_OpenID_AX_AttrInfo::make($type_uri); + + $this->assertEquals($type_uri, $ainfo->type_uri); + $this->assertEquals(1, $ainfo->count); + $this->assertFalse($ainfo->required); + $this->assertTrue($ainfo->alias === null); + } +} + +class ToTypeURIsTest extends PHPUnit_TestCase { + function setUp() + { + $this->aliases = new Auth_OpenID_NamespaceMap(); + } + + function test_empty() + { + foreach (array(null, '') as $empty) { + $uris = Auth_OpenID_AX_toTypeURIs($this->aliases, $empty); + $this->assertEquals(array(), $uris); + } + } + + function test_undefined() + { + $result = Auth_OpenID_AX_toTypeURIs($this->aliases, + 'http://janrain.com/'); + $this->assertTrue(Auth_OpenID_AX::isError($result)); + } + + function test_one() + { + $uri = 'http://janrain.com/'; + $alias = 'openid_hackers'; + $this->aliases->addAlias($uri, $alias); + $uris = Auth_OpenID_AX_toTypeURIs($this->aliases, $alias); + $this->assertEquals(array($uri), $uris); + } + + function test_two() + { + $uri1 = 'http://janrain.com/'; + $alias1 = 'openid_hackers'; + $this->aliases->addAlias($uri1, $alias1); + + $uri2 = 'http://jyte.com/'; + $alias2 = 'openid_hack'; + $this->aliases->addAlias($uri2, $alias2); + + $uris = Auth_OpenID_AX_toTypeURIs($this->aliases, + implode(',', array($alias1, $alias2))); + $this->assertEquals(array($uri1, $uri2), $uris); + } +} + +class ParseAXValuesTest extends PHPUnit_TestCase { + function failUnlessAXKeyError($ax_args) + { + $msg = new Auth_OpenID_AX_KeyValueMessage(); + $result = $msg->parseExtensionArgs($ax_args); + $this->assertTrue(Auth_OpenID_AX::isError($result)); + $this->assertTrue($result->message); + } + + function failUnlessAXValues($ax_args, $expected_args) + { + $msg = new Auth_OpenID_AX_KeyValueMessage(); + $msg->parseExtensionArgs($ax_args); + $this->assertEquals($expected_args, $msg->data); + } + + function test_emptyIsValid() + { + $this->failUnlessAXValues(array(), array()); + } + + function test_invalidAlias() + { + $types = array( + 'Auth_OpenID_AX_KeyValueMessage', + 'Auth_OpenID_AX_FetchRequest' + ); + + $inputs = array( + array('type.a.b' => 'urn:foo', + 'count.a.b' => '1'), + array('type.a,b' => 'urn:foo', + 'count.a,b' => '1'), + ); + + foreach ($types as $typ) { + foreach ($inputs as $input) { + $msg = new $typ(); + $result = $msg->parseExtensionArgs($input); + $this->assertTrue(Auth_OpenID_AX::isError($result)); + } + } + } + + function test_missingValueForAliasExplodes() + { + $this->failUnlessAXKeyError(array('type.foo' => 'urn:foo')); + } + + function test_countPresentButNotValue() + { + $this->failUnlessAXKeyError(array('type.foo' => 'urn:foo', + 'count.foo' => '1')); + } + + function test_invalidCountValue() + { + $msg = new Auth_OpenID_AX_FetchRequest(); + + $result = $msg->parseExtensionArgs( + array('type.foo' => 'urn:foo', + 'count.foo' => 'bogus')); + + $this->assertTrue(Auth_OpenID_AX::isError($result)); + } + + function test_requestUnlimitedValues() + { + $msg = new Auth_OpenID_AX_FetchRequest(); + + $result = $msg->parseExtensionArgs( + array('mode' => 'fetch_request', + 'required' => 'foo', + 'type.foo' => 'urn:foo', + 'count.foo' => Auth_OpenID_AX_UNLIMITED_VALUES)); + + $attrs = $msg->iterAttrs(); + $foo = $attrs[0]; + + $this->assertTrue($foo->count == Auth_OpenID_AX_UNLIMITED_VALUES); + $this->assertTrue($foo->wantsUnlimitedValues()); + } + + function test_longAlias() + { + // Spec minimum length is 32 characters. This is a silly test + // for this library, but it's here for completeness. + $alias = str_repeat('x', Auth_OpenID_AX_MINIMUM_SUPPORTED_ALIAS_LENGTH); + + $msg = new Auth_OpenID_AX_KeyValueMessage(); + $result = $msg->parseExtensionArgs( + array('type.' . $alias => 'urn:foo', + 'count.' . $alias => '1', + 'value.'.$alias.'.1' => 'first') + ); + $this->assertFalse(Auth_OpenID_AX::isError($result)); + } + + function test_countPresentAndIsZero() + { + $this->failUnlessAXValues( + array('type.foo' => 'urn:foo', + 'count.foo' => '0', + ), array('urn:foo' => array())); + } + + function test_singletonEmpty() + { + $this->failUnlessAXValues( + array('type.foo' => 'urn:foo', + 'value.foo' => '', + ), array('urn:foo' => array())); + } + + function test_doubleAlias() + { + $this->failUnlessAXKeyError( + array('type.foo' => 'urn:foo', + 'value.foo' => '', + 'type.bar' => 'urn:foo', + 'value.bar' => '', + )); + } + + function test_doubleSingleton() + { + $this->failUnlessAXValues( + array('type.foo' => 'urn:foo', + 'value.foo' => '', + 'type.bar' => 'urn:bar', + 'value.bar' => '', + ), array('urn:foo' => array(), 'urn:bar' => array())); + } + + function test_singletonValue() + { + $this->failUnlessAXValues( + array('type.foo' => 'urn:foo', + 'value.foo' => 'Westfall', + ), array('urn:foo' => array('Westfall'))); + } +} + +class FetchRequestTest extends PHPUnit_TestCase { + function setUp() + { + $this->msg = new Auth_OpenID_AX_FetchRequest(); + $this->type_a = 'http://janrain.example.com/a'; + $this->alias_a = 'a'; + } + + function test_mode() + { + $this->assertEquals($this->msg->mode, 'fetch_request'); + } + + function test_construct() + { + $this->assertEquals(array(), $this->msg->requested_attributes); + $this->assertEquals(null, $this->msg->update_url); + + $msg = new Auth_OpenID_AX_FetchRequest('hailstorm'); + $this->assertEquals(array(), $msg->requested_attributes); + $this->assertEquals('hailstorm', $msg->update_url); + } + + function test_add() + { + $uri = 'mud://puddle'; + + // Not yet added: + $this->assertFalse(in_array($uri, $this->msg->iterTypes())); + + $attr = Auth_OpenID_AX_AttrInfo::make($uri); + $this->msg->add($attr); + + // Present after adding + $this->assertTrue(in_array($uri, $this->msg->iterTypes())); + } + + function test_addTwice() + { + $uri = 'lightning://storm'; + + $attr = Auth_OpenID_AX_AttrInfo::make($uri); + $this->msg->add($attr); + $this->assertTrue(Auth_OpenID_AX::isError($this->msg->add($attr))); + } + + function test_getExtensionArgs_empty() + { + $expected_args = array( + 'mode' =>'fetch_request', + ); + $this->assertEquals($expected_args, $this->msg->getExtensionArgs()); + } + + function test_getExtensionArgs_noAlias() + { + $attr = Auth_OpenID_AX_AttrInfo::make('type://of.transportation'); + + $this->msg->add($attr); + $ax_args = $this->msg->getExtensionArgs(); + $found = false; + $alias = null; + + foreach ($ax_args as $k => $v) { + if (($v == $attr->type_uri) && (strpos($k, 'type.') === 0)) { + $alias = substr($k, 5); + $found = true; + break; + } + } + + if (!$found) { + $this->fail("Didn't find the type definition"); + return; + } + + $this->failUnlessExtensionArgs(array( + 'type.' . $alias => $attr->type_uri, + 'if_available' => $alias)); + } + + function test_getExtensionArgs_alias_if_available() + { + $attr = Auth_OpenID_AX_AttrInfo::make( + 'type://of.transportation', 1, false, + 'transport'); + + $this->msg->add($attr); + $this->failUnlessExtensionArgs(array( + 'type.' . $attr->alias => $attr->type_uri, + 'if_available' => $attr->alias)); + } + + function test_getExtensionArgs_alias_req() + { + $attr = Auth_OpenID_AX_AttrInfo::make( + 'type://of.transportation', + 1, true, 'transport'); + + $this->msg->add($attr); + $this->failUnlessExtensionArgs(array( + 'type.' . $attr->alias => $attr->type_uri, + 'required' => $attr->alias)); + } + + /* + * Make sure that getExtensionArgs has the expected result + * + * This method will fill in the mode. + */ + function failUnlessExtensionArgs($expected_args) + { + $expected_args['mode'] = $this->msg->mode; + $this->assertEquals($expected_args, $this->msg->getExtensionArgs()); + } + + function test_isIterable() + { + $this->assertEquals(array(), $this->msg->iterAttrs()); + $this->assertEquals(array(), $this->msg->iterTypes()); + } + + function test_getRequiredAttrs_empty() + { + $this->assertEquals(array(), $this->msg->getRequiredAttrs()); + } + + function test_parseExtensionArgs_extraType() + { + $extension_args = array( + 'mode' => 'fetch_request', + 'type.' . $this->alias_a => $this->type_a); + + $this->assertTrue(Auth_OpenID_AX::isError( + $this->msg->parseExtensionArgs($extension_args))); + } + + function test_parseExtensionArgs() + { + $extension_args = array( + 'mode' => 'fetch_request', + 'type.' . $this->alias_a => $this->type_a, + 'if_available' => $this->alias_a); + + $this->msg->parseExtensionArgs($extension_args); + $this->assertEquals(array($this->type_a), $this->msg->iterTypes()); + $attr_info = Auth_OpenID::arrayGet($this->msg->requested_attributes, + $this->type_a); + $this->assertTrue($attr_info); + $this->assertFalse($attr_info->required); + $this->assertEquals($this->type_a, $attr_info->type_uri); + $this->assertEquals($this->alias_a, $attr_info->alias); + $this->assertEquals(array($attr_info), + $this->msg->iterAttrs()); + } + + function test_extensionArgs_idempotent() + { + $extension_args = array( + 'mode' => 'fetch_request', + 'type.' . $this->alias_a => $this->type_a, + 'if_available' => $this->alias_a); + + $this->msg->parseExtensionArgs($extension_args); + $this->assertEquals($extension_args, $this->msg->getExtensionArgs()); + + $attr = $this->msg->requested_attributes[$this->type_a]; + $this->assertFalse($attr->required); + } + + function test_extensionArgs_idempotent_count_required() + { + $extension_args = array( + 'mode' => 'fetch_request', + 'type.' . $this->alias_a => $this->type_a, + 'count.' . $this->alias_a => '2', + 'required' => $this->alias_a); + + $this->msg->parseExtensionArgs($extension_args); + $this->assertEquals($extension_args, $this->msg->getExtensionArgs()); + + $attr = $this->msg->requested_attributes[$this->type_a]; + $this->assertTrue($attr->required); + } + + function test_extensionArgs_count1() + { + $extension_args = array( + 'mode' => 'fetch_request', + 'type.' . $this->alias_a => $this->type_a, + 'count.' . $this->alias_a => '1', + 'if_available' => $this->alias_a); + + $extension_args_norm = array( + 'mode' => 'fetch_request', + 'type.' . $this->alias_a => $this->type_a, + 'if_available' => $this->alias_a); + + $this->msg->parseExtensionArgs($extension_args); + $this->assertEquals($extension_args_norm, $this->msg->getExtensionArgs()); + } + + function test_openidNoRealm() + { + $openid_req_msg = Auth_OpenID_Message::fromOpenIDArgs(array( + 'mode' => 'checkid_setup', + 'ns' => Auth_OpenID_OPENID2_NS, + 'ns.ax' => Auth_OpenID_AX_NS_URI, + 'ax.update_url' => 'http://different.site/path', + 'ax.mode' => 'fetch_request', + )); + $openid_req = new Auth_OpenID_Request(); + $openid_req->message =& $openid_req_msg; + $result = Auth_OpenID_AX_FetchRequest::fromOpenIDRequest( + $openid_req); + $this->assertTrue(Auth_OpenID_AX::isError($result)); + } + + function test_openidUpdateURLVerificationError() + { + $openid_req_msg = Auth_OpenID_Message::fromOpenIDArgs(array( + 'mode' => 'checkid_setup', + 'ns' => Auth_OpenID_OPENID2_NS, + 'realm' => 'http://example.com/realm', + 'ns.ax' => Auth_OpenID_AX_NS_URI, + 'ax.update_url' => 'http://different.site/path', + 'ax.mode' => 'fetch_request', + )); + $openid_req = new Auth_OpenID_Request(); + $openid_req->message =& $openid_req_msg; + $result = Auth_OpenID_AX_FetchRequest::fromOpenIDRequest($openid_req); + $this->assertTrue(Auth_OpenID_AX::isError($result)); + } + + function test_openidUpdateURLVerificationSuccess() + { + $openid_req_msg = Auth_OpenID_Message::fromOpenIDArgs(array( + 'mode' => 'checkid_setup', + 'ns' => Auth_OpenID_OPENID2_NS, + 'realm' => 'http://example.com/realm', + 'ns.ax' => Auth_OpenID_AX_NS_URI, + 'ax.update_url' => 'http://example.com/realm/update_path', + 'ax.mode' => 'fetch_request', + )); + $openid_req = new Auth_OpenID_Request(); + $openid_req->message =& $openid_req_msg; + $fr = Auth_OpenID_AX_FetchRequest::fromOpenIDRequest($openid_req); + $this->assertFalse(Auth_OpenID_AX::isError($fr)); + } + + function test_openidUpdateURLVerificationSuccessReturnTo() + { + $openid_req_msg = Auth_OpenID_Message::fromOpenIDArgs(array( + 'mode' => 'checkid_setup', + 'ns' => Auth_OpenID_OPENID2_NS, + 'return_to' => 'http://example.com/realm', + 'ns.ax' => Auth_OpenID_AX_NS_URI, + 'ax.update_url' => 'http://example.com/realm/update_path', + 'ax.mode' => 'fetch_request', + )); + $openid_req = new Auth_OpenID_Request(); + $openid_req->message =& $openid_req_msg; + $fr = Auth_OpenID_AX_FetchRequest::fromOpenIDRequest($openid_req); + $this->assertFalse(Auth_OpenID_AX::isError($fr)); + } +} + +class FauxEndpoint { + function FauxEndpoint() { + $this->claimed_id = 'http://some.url/'; + } +} + +class FetchResponseTest extends PHPUnit_TestCase { + function setUp() + { + $this->msg = new Auth_OpenID_AX_FetchResponse(); + $this->value_a = 'monkeys'; + $this->type_a = 'http://phone.home/'; + $this->alias_a = 'robocop'; + $this->request_update_url = 'http://update.bogus/'; + } + + function test_construct() + { + $this->assertTrue($this->msg->update_url === null); + $this->assertEquals(array(), $this->msg->data); + } + + function test_getExtensionArgs_empty() + { + $expected_args = array( + 'mode' => 'fetch_response', + ); + $req = null; + $this->assertEquals($expected_args, $this->msg->getExtensionArgs($req)); + } + + function test_getExtensionArgs_empty_request() + { + $expected_args = array( + 'mode' => 'fetch_response', + ); + $req = new Auth_OpenID_AX_FetchRequest(); + $this->assertEquals($expected_args, $this->msg->getExtensionArgs($req)); + } + + function test_getExtensionArgs_empty_request_some() + { + $uri = 'http://not.found/'; + $alias = 'ext0'; + + $expected_args = array( + 'mode' => 'fetch_response', + 'type.' . $alias => $uri, + 'count.' . $alias => '0' + ); + $req = new Auth_OpenID_AX_FetchRequest(); + $req->add(Auth_OpenID_AX_AttrInfo::make('http://not.found/')); + $this->assertEquals($expected_args, $this->msg->getExtensionArgs($req)); + } + + function test_updateUrlInResponse() + { + $uri = 'http://not.found/'; + $alias = 'ext0'; + + $expected_args = array( + 'mode' => 'fetch_response', + 'update_url' => $this->request_update_url, + 'type.' . $alias => $uri, + 'count.' . $alias => '0' + ); + $req = new Auth_OpenID_AX_FetchRequest($this->request_update_url); + $req->add(Auth_OpenID_AX_AttrInfo::make($uri)); + $this->assertEquals($expected_args, $this->msg->getExtensionArgs($req)); + } + + function test_getExtensionArgs_some_request() + { + $expected_args = array( + 'mode' => 'fetch_response', + 'type.' . $this->alias_a => $this->type_a, + 'value.' . $this->alias_a . '.1' => $this->value_a, + 'count.' . $this->alias_a => '1' + ); + + $req = new Auth_OpenID_AX_FetchRequest(); + $req->add(Auth_OpenID_AX_AttrInfo::make($this->type_a, 1, false, $this->alias_a)); + $this->msg->addValue($this->type_a, $this->value_a); + + $result = $this->msg->getExtensionArgs($req); + $this->assertEquals($expected_args, $result); + } + + function test_getExtensionArgs_some_not_request() + { + $req = new Auth_OpenID_AX_FetchRequest(); + $this->msg->addValue($this->type_a, $this->value_a); + $this->assertTrue(Auth_OpenID_AX::isError($this->msg->getExtensionArgs($req))); + } + + function test_getSingle_success() + { + $req = new Auth_OpenID_AX_FetchRequest(); + $this->msg->addValue($this->type_a, $this->value_a); + $this->assertEquals($this->value_a, $this->msg->getSingle($this->type_a)); + } + + function test_getSingle_none() + { + $this->assertEquals(null, $this->msg->getSingle($this->type_a)); + } + + function test_getSingle_extra() + { + $data = array('x', 'y'); + $this->msg->setValues($this->type_a, $data); + $this->assertTrue(Auth_OpenID_AX::isError($this->msg->getSingle($this->type_a))); + } + + function test_get() + { + $this->assertTrue(Auth_OpenID_AX::isError($this->msg->get($this->type_a))); + } + + function test_fromSuccessResponseWithoutExtension() + { + $args = array( + 'mode' => 'id_res', + 'ns' => Auth_OpenID_OPENID2_NS + ); + $sf = array(); + foreach (array_keys($args) as $k) { + array_push($sf, $k); + } + $msg = Auth_OpenID_Message::fromOpenIDArgs($args); + $e = new FauxEndpoint(); + $resp = new Auth_OpenID_SuccessResponse($e, $msg, $sf); + $ax_resp = Auth_OpenID_AX_FetchResponse::fromSuccessResponse($resp); + $this->assertTrue($ax_resp === null); + } + + function test_fromSuccessResponseWithoutData() + { + $args = array( + 'mode' => 'id_res', + 'ns' => Auth_OpenID_OPENID2_NS, + 'ns.ax' => Auth_OpenID_AX_NS_URI, + 'ax.mode' => 'fetch_response', + ); + $sf = array(); + foreach (array_keys($args) as $k) { + array_push($sf, $k); + } + $msg = Auth_OpenID_Message::fromOpenIDArgs($args); + $e = new FauxEndpoint(); + $resp = new Auth_OpenID_SuccessResponse($e, $msg, $sf); + $ax_resp = Auth_OpenID_AX_FetchResponse::fromSuccessResponse($resp); + $this->assertTrue($ax_resp === null); + } + + function test_fromSuccessResponse() + { + $name = "ziggy"; + $value = "stardust"; + $uri = "http://david.bowie.name/"; + $args = array( + 'mode' => 'id_res', + 'ns' => Auth_OpenID_OPENID2_NS, + 'ns.ax' => Auth_OpenID_AX_NS_URI, + 'ax.mode' => 'fetch_response', + 'ax.update_url' => 'http://example.com/realm/update_path', + 'ax.type.'.$name => $uri, + 'ax.count.'.$name => '1', + 'ax.value.'.$name.'.1' => $value, + ); + $sf = array(); + foreach (array_keys($args) as $k) { + array_push($sf, $k); + } + $msg = Auth_OpenID_Message::fromOpenIDArgs($args); + $e = new FauxEndpoint(); + $resp = new Auth_OpenID_SuccessResponse($e, $msg, $sf); + $ax_resp = Auth_OpenID_AX_FetchResponse::fromSuccessResponse($resp, false); + $this->assertFalse($ax_resp === null); + $this->assertTrue(is_a($ax_resp, 'Auth_OpenID_AX_FetchResponse')); + $values = $ax_resp->get($uri); + $this->assertEquals(array($value), $values); + } +} + +class StoreRequestTest extends PHPUnit_TestCase { + function setUp() + { + $this->msg = new Auth_OpenID_AX_StoreRequest(); + $this->type_a = 'http://three.count/'; + $this->alias_a = 'juggling'; + } + + function test_construct() + { + $this->assertEquals(array(), $this->msg->data); + } + + function test_getExtensionArgs_empty() + { + $args = $this->msg->getExtensionArgs(); + $expected_args = array( + 'mode' => 'store_request', + ); + $this->assertEquals($expected_args, $args); + } + + function test_getExtensionArgs_nonempty() + { + $data = array('foo', 'bar'); + $this->msg->setValues($this->type_a, $data); + $aliases = new Auth_OpenID_NamespaceMap(); + $aliases->addAlias($this->type_a, $this->alias_a); + $args = $this->msg->getExtensionArgs($aliases); + $expected_args = array( + 'mode' => 'store_request', + 'type.' . $this->alias_a => $this->type_a, + 'count.' . $this->alias_a => '2', + sprintf('value.%s.1', $this->alias_a) => 'foo', + sprintf('value.%s.2', $this->alias_a) => 'bar', + ); + $this->assertEquals($expected_args, $args); + } +} + +class StoreResponseTest extends PHPUnit_TestCase { + function test_success() + { + $msg = new Auth_OpenID_AX_StoreResponse(); + $this->assertTrue($msg->succeeded()); + $this->assertFalse($msg->error_message); + $this->assertEquals(array('mode' => 'store_response_success'), + $msg->getExtensionArgs()); + } + + function test_fail_nomsg() + { + $msg = new Auth_OpenID_AX_StoreResponse(false); + $this->assertFalse($msg->succeeded()); + $this->assertFalse($msg->error_message); + $this->assertEquals(array('mode' => 'store_response_failure'), + $msg->getExtensionArgs()); + } + + function test_fail_msg() + { + $reason = 'no reason, really'; + $msg = new Auth_OpenID_AX_StoreResponse(false, $reason); + $this->assertFalse($msg->succeeded()); + $this->assertEquals($reason, $msg->error_message); + $this->assertEquals(array('mode' => 'store_response_failure', + 'error' => $reason), $msg->getExtensionArgs()); + } +} + +class Tests_Auth_OpenID_AX extends PHPUnit_TestSuite { + function getName() + { + return "Tests_Auth_OpenID_AX"; + } + + function Tests_Auth_OpenID_AX() + { + $this->addTestSuite('StoreResponseTest'); + $this->addTestSuite('StoreRequestTest'); + $this->addTestSuite('FetchResponseTest'); + $this->addTestSuite('FetchRequestTest'); + $this->addTestSuite('ParseAXValuesTest'); + $this->addTestSuite('ToTypeURIsTest'); + $this->addTestSuite('AttrInfoTest'); + $this->addTestSuite('AXMessageTest'); + } +} + +?>
\ No newline at end of file diff --git a/Tests/Auth/OpenID/Association.php b/Tests/Auth/OpenID/Association.php new file mode 100644 index 0000000..9fc6e17 --- /dev/null +++ b/Tests/Auth/OpenID/Association.php @@ -0,0 +1,55 @@ +<?php + +/** + * Tests for the Association implementation. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'PHPUnit.php'; +require_once 'Auth/OpenID/Association.php'; + +class Tests_Auth_OpenID_Association extends PHPUnit_TestCase { + function test_me() + { + $issued = time(); + $lifetime = 600; + $assoc = new Auth_OpenID_Association('handle', 'secret', $issued, + $lifetime, 'HMAC-SHA1'); + $s = $assoc->serialize(); + $assoc2 = Auth_OpenID_Association::deserialize( + 'Auth_OpenID_Association', $s); + + if ($assoc2 === null) { + $this->fail('deserialize returned null'); + } else { + $this->assertTrue($assoc2->equal($assoc)); + } + } + function test_me256() + { + if(!Auth_OpenID_HMACSHA256_SUPPORTED) return; + $issued = time(); + $lifetime = 600; + $assoc = new Auth_OpenID_Association('handle', 'secret', $issued, + $lifetime, 'HMAC-SHA256'); + $s = $assoc->serialize(); + $assoc2 = Auth_OpenID_Association::deserialize( + 'Auth_OpenID_Association', $s); + + if ($assoc2 === null) { + $this->fail('deserialize returned null'); + } else { + $this->assertTrue($assoc2->equal($assoc)); + } + } +} + +?> diff --git a/Tests/Auth/OpenID/AssociationResponse.php b/Tests/Auth/OpenID/AssociationResponse.php new file mode 100644 index 0000000..da80d15 --- /dev/null +++ b/Tests/Auth/OpenID/AssociationResponse.php @@ -0,0 +1,379 @@ +<?php + +require_once "PHPUnit.php"; +require_once "Tests/Auth/OpenID/TestUtil.php"; +require_once "Tests/Auth/OpenID/MemStore.php"; + +require_once "Auth/OpenID/Message.php"; +require_once "Auth/OpenID/Server.php"; +require_once "Auth/OpenID/Consumer.php"; +require_once "Auth/OpenID/Association.php"; + +// Some values we can use for convenience (see mkAssocResponse) +global $association_response_values; +$association_response_values = array( + 'expires_in' => '1000', + 'assoc_handle' => 'a handle', + 'assoc_type' => 'a type', + 'session_type' => 'a session type', + 'ns' => Auth_OpenID_OPENID2_NS + ); + +/** + * Build an association response message that contains the specified + * subset of keys. The values come from association_response_values. + * + * This is useful for testing for missing keys and other times that we + * don't care what the values are. + */ +function mkAssocResponse($keys) +{ + global $association_response_values; + + $args = array(); + + foreach ($keys as $key) { + $args[$key] = $association_response_values[$key]; + } + + return Auth_OpenID_Message::fromOpenIDArgs($args); +} + +class Tests_Auth_OpenID_AssociationResponse extends PHPUnit_TestCase { + function setUp() + { + $this->store = new Tests_Auth_OpenID_MemStore(); + $this->consumer = new Auth_OpenID_GenericConsumer($this->store); + $this->endpoint = new Auth_OpenID_ServiceEndpoint(); + } + + function failUnlessProtocolError($thing) + { + $this->assertTrue(Auth_OpenID::isFailure($thing)); + } + + function _run($keys) + { + $msg = mkAssocResponse($keys); + $dumb = null; + $this->assertTrue(Auth_OpenID::isFailure($this->consumer->_extractAssociation($msg, $dumb))); + } +} + +/** + * Test for returning an error upon missing fields in association + * responses for OpenID 2 + */ +class TestExtractAssociationMissingFieldsOpenID2 extends Tests_Auth_OpenID_AssociationResponse { + + function test_noFields_openid2() + { + $this->_run(array('ns')); + } + + function test_missingExpires_openid2() + { + $this->_run(array('assoc_handle', 'assoc_type', 'session_type', 'ns')); + } + + function test_missingHandle_openid2() + { + $this->_run(array('expires_in', 'assoc_type', 'session_type', 'ns')); + } + + function test_missingAssocType_openid2() + { + $this->_run(array('expires_in', 'assoc_handle', 'session_type', 'ns')); + } + + function test_missingSessionType_openid2() + { + $this->_run(array('expires_in', 'assoc_handle', 'assoc_type', 'ns')); + } +} + +/** + * Test for returning an error upon missing fields in association + * responses for OpenID 2 + */ +class TestExtractAssociationMissingFieldsOpenID1 extends Tests_Auth_OpenID_AssociationResponse { + function test_noFields_openid1() + { + $this->_run(array()); + } + + function test_missingExpires_openid1() + { + $this->_run(array('assoc_handle', 'assoc_type')); + } + + function test_missingHandle_openid1() + { + $this->_run(array('expires_in', 'assoc_type')); + } + + function test_missingAssocType_openid1() + { + $this->_run(array('expires_in', 'assoc_handle')); + } +} + +class DummyAssocationSession { + function DummyAssocationSession($session_type, $allowed_assoc_types=array()) + { + $this->session_type = $session_type; + $this->allowed_assoc_types = $allowed_assoc_types; + } +} + +class ExtractAssociationSessionTypeMismatch extends Tests_Auth_OpenID_AssociationResponse { + function _run($requested_session_type, $response_session_type, $openid1=false) + { + global $association_response_values; + + $assoc_session = new DummyAssocationSession($requested_session_type); + $keys = array_keys($association_response_values); + if ($openid1) { + if (in_array('ns', $keys)) { + unset($keys[array_search('ns', $keys)]); + } + } + + $msg = mkAssocResponse($keys); + $msg->setArg(Auth_OpenID_OPENID_NS, 'session_type', + $response_session_type); + $this->assertTrue( + $this->consumer->_extractAssociation($msg, $assoc_session) === null); + } + + function test_typeMismatchNoEncBlank_openid2() + { + $this->_run('no-encryption', ''); + } + + function test_typeMismatchDHSHA1NoEnc_openid2() + { + $this->_run('DH-SHA1', 'no-encryption'); + } + + function test_typeMismatchDHSHA256NoEnc_openid2() + { + $this->_run('DH-SHA256', 'no-encryption'); + } + + function test_typeMismatchNoEncDHSHA1_openid2() + { + $this->_run('no-encryption', 'DH-SHA1'); + } + + function test_typeMismatchDHSHA1NoEnc_openid1() + { + $this->_run('DH-SHA1', 'DH-SHA256', true); + } + + function test_typeMismatchDHSHA256NoEnc_openid1() + { + $this->_run('DH-SHA256', 'DH-SHA1', true); + } + + function test_typeMismatchNoEncDHSHA1_openid1() + { + $this->_run('no-encryption', 'DH-SHA1', true); + } +} + +class TestOpenID1AssociationResponseSessionType extends Tests_Auth_OpenID_AssociationResponse { + function _run($expected_session_type, $session_type_value) + { + // Create a Message with just 'session_type' in it, since + // that's all this function will use. 'session_type' may be + // absent if it's set to None. + $args = array(); + if ($session_type_value !== null) { + $args['session_type'] = $session_type_value; + } + $message = Auth_OpenID_Message::fromOpenIDArgs($args); + $this->assertTrue($message->isOpenID1()); + + $actual_session_type = $this->consumer->_getOpenID1SessionType($message); + $error_message = sprintf('Returned sesion type parameter %s was expected ' . + 'to yield session type %s, but yielded %s', + $session_type_value, $expected_session_type, + $actual_session_type); + $this->assertEquals( + $expected_session_type, + $actual_session_type, + $error_message); + } + + function test_none() + { + $this->_run('no-encryption', null); + } + + function test_empty() + { + $this->_run('no-encryption', ''); + } + + function test_explicitNoEncryption() + { + $this->_run('no-encryption', 'no-encryption'); + } + + function test_dhSHA1() + { + $this->_run('DH-SHA1', 'DH-SHA1'); + } + + // DH-SHA256 is not a valid session type for OpenID1, but this + // function does not test that. This is mostly just to make sure + // that it will pass-through stuff that is not explicitly handled, + // so it will get handled the same way as it is handled for OpenID + // 2 + function test_dhSHA256() + { + $this->_run('DH-SHA256', 'DH-SHA256'); + } +} + +class DummyAssociationSession { + var $secret = "shh! don't tell!"; + var $extract_secret_called = false; + var $session_type = null; + var $allowed_assoc_types = null; + + function extractSecret($message) + { + $this->extract_secret_called = true; + return $this->secret; + } +} + +class TestInvalidFields extends Tests_Auth_OpenID_AssociationResponse { + function setUp() + { + parent::setUp(); + $this->session_type = 'testing-session'; + + // This must something that works for Association.fromExpiresIn + $this->assoc_type = 'HMAC-SHA1'; + + $this->assoc_handle = 'testing-assoc-handle'; + + // These arguments should all be valid + $this->assoc_response = Auth_OpenID_Message::fromOpenIDArgs(array( + 'expires_in' => '1000', + 'assoc_handle' => $this->assoc_handle, + 'assoc_type' => $this->assoc_type, + 'session_type' => $this->session_type, + 'ns' => Auth_OpenID_OPENID2_NS, + )); + + $this->assoc_session = new DummyAssociationSession(); + + // Make the session for the response's session type + $this->assoc_session->session_type = $this->session_type; + $this->assoc_session->allowed_assoc_types = array($this->assoc_type); + } + + function test_worksWithGoodFields() + { + // Handle a full successful association response + $assoc = $this->consumer->_extractAssociation( + $this->assoc_response, $this->assoc_session); + $this->assertTrue($this->assoc_session->extract_secret_called); + $this->assertEquals($this->assoc_session->secret, $assoc->secret); + $this->assertEquals(1000, $assoc->lifetime); + $this->assertEquals($this->assoc_handle, $assoc->handle); + $this->assertEquals($this->assoc_type, $assoc->assoc_type); + } + + function test_badAssocType() + { + // Make sure that the assoc type in the response is not valid + // for the given session. + $this->assoc_session->allowed_assoc_types = array(); + $this->assertTrue( + $this->consumer->_extractAssociation($this->assoc_response, + $this->assoc_session) === null); + } + + function test_badExpiresIn() + { + // Invalid value for expires_in should cause failure + $this->assoc_response->setArg(Auth_OpenID_OPENID_NS, 'expires_in', 'forever'); + $assoc = $this->consumer->_extractAssociation($this->assoc_response, + $this->assoc_session); + $this->assertTrue(Auth_OpenID::isFailure($assoc)); + } +} + +class TestExtractAssociationDiffieHellman extends Tests_Auth_OpenID_AssociationResponse { + var $secret = 'xxxxxxxxxxxxxxxxxxxx'; + + function _setUpDH() + { + list($sess, $message) = $this->consumer->_createAssociateRequest( + $this->endpoint, 'HMAC-SHA1', 'DH-SHA1'); + + // XXX: this is testing _createAssociateRequest + $this->assertEquals($this->endpoint->compatibilityMode(), + $message->isOpenID1()); + + $server_sess = Auth_OpenID_DiffieHellmanSHA1ServerSession::fromMessage($message); + $server_resp = $server_sess->answer($this->secret); + $server_resp['assoc_type'] = 'HMAC-SHA1'; + $server_resp['assoc_handle'] = 'handle'; + $server_resp['expires_in'] = '1000'; + $server_resp['session_type'] = 'DH-SHA1'; + return array($sess, Auth_OpenID_Message::fromOpenIDArgs($server_resp)); + } + + function test_success() + { + list($sess, $server_resp) = $this->_setUpDH(); + $ret = $this->consumer->_extractAssociation($server_resp, $sess); + $this->assertTrue($ret !== null); + $this->assertEquals($ret->assoc_type, 'HMAC-SHA1'); + $this->assertEquals($ret->secret, $this->secret); + $this->assertEquals($ret->handle, 'handle'); + $this->assertEquals($ret->lifetime, 1000); + } + + function test_openid2success() + { + // Use openid 2 type in endpoint so _setUpDH checks + // compatibility mode state properly + $this->endpoint->type_uris = array(Auth_OpenID_TYPE_2_0, + Auth_OpenID_TYPE_1_1); + $this->test_success(); + } + + /** + * Can't run this test because the base64 decoder is broken. + */ + /* + function test_badDHValues() + { + list($sess, $server_resp) = $this->_setUpDH(); + $server_resp->setArg(Auth_OpenID_OPENID_NS, 'enc_mac_key', "\x00\x00\x00"); + $this->assertTrue($this->consumer->_extractAssociation($server_resp, $sess) === null); + } + */ +} + +global $Tests_Auth_OpenID_AssociationResponse_other; +$Tests_Auth_OpenID_AssociationResponse_other = array( + new TestInvalidFields(), + new TestOpenID1AssociationResponseSessionType(), + new ExtractAssociationSessionTypeMismatch(), + new TestExtractAssociationMissingFieldsOpenID1(), + new TestExtractAssociationMissingFieldsOpenID2() + ); + +if (!defined('Auth_OpenID_NO_MATH_SUPPORT')) { + $Tests_Auth_OpenID_AssociationResponse_other[] = new TestExtractAssociationDiffieHellman(); +} + +?>
\ No newline at end of file diff --git a/Tests/Auth/OpenID/AuthRequest.php b/Tests/Auth/OpenID/AuthRequest.php new file mode 100644 index 0000000..63e4d4e --- /dev/null +++ b/Tests/Auth/OpenID/AuthRequest.php @@ -0,0 +1,302 @@ +<?php + +require_once 'PHPUnit.php'; + +require_once 'Tests/Auth/OpenID/TestUtil.php'; + +require_once 'Auth/OpenID/Association.php'; +require_once 'Auth/OpenID/Consumer.php'; + +class AuthRequest_DummyEndpoint { + var $preferred_namespace = null; + var $local_id = null; + var $server_url = null; + var $is_op_identifier = false; + + function preferredNamespace() + { + return $this->preferred_namespace; + } + + function getLocalID() + { + return $this->local_id; + } + + function isOPIdentifier() + { + return $this->is_op_identifier; + } +} + +class AuthRequest_DummyAssoc { + var $handle = "assoc-handle"; +} + +/** + * Base for AuthRequest tests for OpenID 1 and 2. + */ +class TestAuthRequestMixin extends OpenIDTestMixin { + + var $preferred_namespace = null; + var $immediate = false; + var $expected_mode = 'checkid_setup'; + + function setUp() + { + $this->endpoint = new AuthRequest_DummyEndpoint(); + $this->endpoint->local_id = 'http://server.unittest/joe'; + $this->endpoint->claimed_id = 'http://joe.vanity.example/'; + $this->endpoint->server_url = 'http://server.unittest/'; + $this->endpoint->preferred_namespace = $this->preferred_namespace; + $this->realm = 'http://example/'; + $this->return_to = 'http://example/return/'; + $this->assoc = new AuthRequest_DummyAssoc(); + $this->authreq = new Auth_OpenID_AuthRequest($this->endpoint, $this->assoc); + } + + function failUnlessAnonymous($msg) + { + foreach (array('claimed_id', 'identity') as $key) { + $this->failIfOpenIDKeyExists($msg, $key); + } + } + + function failUnlessHasRequiredFields($msg) + { + $this->assertEquals($this->preferred_namespace, + $this->authreq->message->getOpenIDNamespace()); + + $this->assertEquals($this->preferred_namespace, + $msg->getOpenIDNamespace()); + + $this->failUnlessOpenIDValueEquals($msg, 'mode', + $this->expected_mode); + + // Implement these in subclasses because they depend on + // protocol differences! + $this->failUnlessHasRealm($msg); + $this->failUnlessIdentifiersPresent($msg); + } + + // TESTS + + function test_checkNoAssocHandle() + { + $this->authreq->assoc = null; + $msg = $this->authreq->getMessage($this->realm, $this->return_to, + $this->immediate); + + $this->failIfOpenIDKeyExists($msg, 'assoc_handle'); + } + + function test_checkWithAssocHandle() + { + $msg = $this->authreq->getMessage($this->realm, $this->return_to, + $this->immediate); + + $this->failUnlessOpenIDValueEquals($msg, 'assoc_handle', + $this->assoc->handle); + } + + function test_addExtensionArg() + { + $this->authreq->addExtensionArg('bag:', 'color', 'brown'); + $this->authreq->addExtensionArg('bag:', 'material', 'paper'); + $this->assertTrue($this->authreq->message->namespaces->contains('bag:')); + $this->assertEquals($this->authreq->message->getArgs('bag:'), + array('color' => 'brown', + 'material' => 'paper')); + $msg = $this->authreq->getMessage($this->realm, $this->return_to, + $this->immediate); + + // XXX: this depends on the way that Message assigns + // namespaces. Really it doesn't care that it has alias "0", + // but that is tested anyway + $post_args = $msg->toPostArgs(); + $this->assertEquals('brown', $post_args['openid.ext0.color']); + $this->assertEquals('paper', $post_args['openid.ext0.material']); + } + + function test_standard() + { + $msg = $this->authreq->getMessage($this->realm, $this->return_to, + $this->immediate); + + $this->failUnlessHasIdentifiers( + $msg, $this->endpoint->local_id, + $this->endpoint->claimed_id); + } +} + +class TestAuthRequestOpenID2 extends TestAuthRequestMixin { + var $preferred_namespace = Auth_OpenID_OPENID2_NS; + + function failUnlessHasRealm($msg) + { + // check presence of proper realm key and absence of the wrong + // one. + $this->failUnlessOpenIDValueEquals($msg, 'realm', $this->realm); + $this->failIfOpenIDKeyExists($msg, 'trust_root'); + } + + function failUnlessIdentifiersPresent($msg) + { + $identity_present = $msg->hasKey(Auth_OpenID_OPENID_NS, 'identity'); + $claimed_present = $msg->hasKey(Auth_OpenID_OPENID_NS, 'claimed_id'); + + $this->assertEquals($claimed_present, $identity_present); + } + + function failUnlessHasIdentifiers($msg, $op_specific_id, $claimed_id) + { + $this->failUnlessOpenIDValueEquals($msg, 'identity', $op_specific_id); + $this->failUnlessOpenIDValueEquals($msg, 'claimed_id', $claimed_id); + } + + // TESTS + + function test_markup_checkidImmediate() + { + $result = $this->authreq->formMarkup($this->realm, + null, true); + $this->assertTrue(Auth_OpenID::isFailure($result)); + } + + function test_markup_returnToArgs() + { + $this->authreq->return_to_args = array('extra' => 'args'); + $result = $this->authreq->formMarkup($this->realm, + null, false); + $this->assertTrue(Auth_OpenID::isFailure($result)); + } + + function test_setAnonymousWorksForOpenID2() + { + // OpenID AuthRequests should be able to set 'anonymous' to true. + $this->assertTrue($this->authreq->message->isOpenID2()); + $this->assertTrue($this->authreq->setAnonymous(true)); + $this->assertTrue($this->authreq->setAnonymous(false)); + } + + function test_userAnonymousIgnoresIdentfier() + { + $this->authreq->setAnonymous(true); + $msg = $this->authreq->getMessage($this->realm, $this->return_to, + $this->immediate); + $this->failUnlessHasRequiredFields($msg); + $this->failUnlessAnonymous($msg); + } + + function test_opAnonymousIgnoresIdentifier() + { + $this->endpoint->is_op_identifier = true; + $this->authreq->setAnonymous(true); + $msg = $this->authreq->getMessage($this->realm, $this->return_to, + $this->immediate); + $this->failUnlessHasRequiredFields($msg); + $this->failUnlessAnonymous($msg); + } + + function test_opIdentifierSendsIdentifierSelect() + { + $this->endpoint->is_op_identifier = true; + $msg = $this->authreq->getMessage($this->realm, $this->return_to, + $this->immediate); + $this->failUnlessHasRequiredFields($msg); + $this->failUnlessHasIdentifiers($msg, + Auth_OpenID_IDENTIFIER_SELECT, + Auth_OpenID_IDENTIFIER_SELECT); + } +} + +class TestAuthRequestOpenID1 extends TestAuthRequestMixin { + var $preferred_namespace = Auth_OpenID_OPENID1_NS; + + function setUpEndpoint() + { + parent::setUpEndpoint(); + $this->endpoint->preferred_namespace = Auth_OpenID_OPENID1_NS; + } + + function failUnlessHasIdentifiers($msg, $op_specific_id, $claimed_id) + { + // Make sure claimed_is is *absent* in request. + $this->failUnlessOpenIDValueEquals($msg, 'identity', $op_specific_id); + $this->failIfOpenIDKeyExists($msg, 'claimed_id'); + } + + function failUnlessIdentifiersPresent($msg) + { + $this->failIfOpenIDKeyExists($msg, 'claimed_id'); + $this->assertTrue($msg->hasKey(Auth_OpenID_OPENID_NS, 'identity')); + } + + function failUnlessHasRealm($msg) + { + // check presence of proper realm key and absence of the wrong + // one. + $this->failUnlessOpenIDValueEquals($msg, 'trust_root', $this->realm); + $this->failIfOpenIDKeyExists($msg, 'realm'); + } + + // TESTS + + function test_markup_missingReturnTo() + { + $result = $this->authreq->formMarkup($this->realm, + null, false); + $this->assertTrue(Auth_OpenID::isFailure($result)); + } + + function test_setAnonymousFailsForOpenID1() + { + // OpenID 1 requests MUST NOT be able to set anonymous to True + $this->assertTrue($this->authreq->message->isOpenID1()); + $this->assertFalse($this->authreq->setAnonymous(true)); + $this->assertTrue($this->authreq->setAnonymous(false)); + } + + function test_identifierSelect() + { + // Identfier select SHOULD NOT be sent, but this pathway is in + // here in case some special discovery stuff is done to + // trigger it with OpenID 1. If it is triggered, it will send + // identifier_select just like OpenID 2. + $this->endpoint->is_op_identifier = true; + $msg = $this->authreq->getMessage($this->realm, $this->return_to, + $this->immediate); + $this->failUnlessHasRequiredFields($msg); + $this->assertEquals(Auth_OpenID_IDENTIFIER_SELECT, + $msg->getArg(Auth_OpenID_OPENID1_NS, + 'identity')); + } +} + +class TestAuthRequestOpenID1Immediate extends TestAuthRequestOpenID1 { + var $immediate = true; + var $expected_mode = 'checkid_immediate'; +} + +class TestAuthRequestOpenID2Immediate extends TestAuthRequestOpenID2 { + var $immediate = true; + var $expected_mode = 'checkid_immediate'; +} + +class Tests_Auth_OpenID_AuthRequest extends PHPUnit_TestSuite { + + function getName() + { + return "Tests_Auth_OpenID_AuthRequest"; + } + + function Tests_Auth_OpenID_AuthRequest() + { + $this->addTestSuite('TestAuthRequestOpenID1'); + $this->addTestSuite('TestAuthRequestOpenID1Immediate'); + $this->addTestSuite('TestAuthRequestOpenID2'); + $this->addTestSuite('TestAuthRequestOpenID2Immediate'); + } +} + +?>
\ No newline at end of file diff --git a/Tests/Auth/OpenID/BigMath.php b/Tests/Auth/OpenID/BigMath.php new file mode 100644 index 0000000..2a51871 --- /dev/null +++ b/Tests/Auth/OpenID/BigMath.php @@ -0,0 +1,235 @@ +<?php + +/** + * Tests for the BigMath functions. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'PHPUnit.php'; +require_once 'Auth/OpenID/BigMath.php'; +require_once 'Tests/Auth/OpenID/TestUtil.php'; + +class Tests_Auth_OpenID_BinLongConvertRnd extends PHPUnit_TestCase { + var $lib; + var $max; + + function Tests_Auth_OpenID_BinLongConvertRnd(&$lib, $max) + { + $this->lib =& $lib; + $this->max = $max; + } + + function runTest() + { + $n = $this->lib->init(0); + foreach (range(0, 9) as $i) { + $rnd = $this->lib->rand($this->max); + $n = $this->lib->add($n, $rnd); + } + $s = $this->lib->longToBinary($n); + $this->assertTrue(is_string($s)); + $n_prime = $this->lib->binaryToLong($s); + $this->assertEquals($this->lib->cmp($n, $n_prime), 0); + } +} + +class Tests_Auth_OpenID_BinLongConvert extends PHPUnit_TestCase { + var $lib; + var $bin; + var $lng; + + function Tests_Auth_OpenID_BinLongConvert(&$lib, $bin, $lng) + { + $this->lib =& $lib; + $this->bin = $bin; + $this->lng = $lng; + } + + function runTest() + { + $n_prime = $this->lib->binaryToLong($this->bin); + $s_prime = $this->lib->longToBinary($this->lng); + $this->assertEquals($this->lib->cmp($this->lng, $n_prime), 0); + $this->assertTrue($this->bin == $s_prime); + } +} + +class Tests_Auth_OpenID_Base64ToLong extends PHPUnit_TestCase { + var $num; + var $b64; + var $lib; + + function Tests_Auth_OpenID_Base64ToLong(&$lib, $b64, $num) + { + $this->lib = $lib; + $this->b64 = $b64; + $this->num = $num; + } + + function runTest() + { + $actual = $this->lib->base64ToLong($this->b64); + $this->assertTrue($this->lib->cmp($this->num, $actual) == 0); + } +} + +class Tests_Auth_OpenID_LongToBase64 extends Tests_Auth_OpenID_Base64ToLong { + function Tests_Auth_OpenID_LongToBase64(&$lib, $b64, $num) + { + $this->lib = $lib; + $this->b64 = $b64; + $this->num = $num; + } + + function runTest() + { + $actual = $this->lib->longToBase64($this->num); + $this->assertEquals($this->b64, $actual); + } +} + +class Tests_Auth_OpenID_Rand extends PHPUnit_TestCase { + function Tests_Auth_OpenID_Rand(&$lib) + { + $this->lib =& $lib; + } + + function runTest() + { + $stop = $this->lib->pow(2, 128); + $a = $this->lib->rand($stop); + $b = $this->lib->rand($stop); + + $this->assertFalse($this->lib->cmp($b, $a) == 0, "Same: $a $b"); + + $n = $this->lib->init(Tests_Auth_OpenID_maxint()); + $n = $this->lib->add($n, 1); + + // Make sure that we can generate random numbers that are + // larger than platform int size + $result = $this->lib->rand($n); + + // What can we say about the result? + } +} + +/** + * Computes the maximum integer value for this PHP installation. + * + * @return int $max_int_value The maximum integer value for this + * PHP installation + */ +function Tests_Auth_OpenID_maxint() +{ + /* assumes largest integer is of form 2^n - 1 */ + $to_test = pow(2, 16); + while (1) { + $last = $to_test; + $to_test = 2 * $to_test; + if (($to_test < $last) || (!is_int($to_test))) { + return($last + ($last - 1)); + } + } +} + + +class Tests_Auth_OpenID_BigMath extends PHPUnit_TestSuite { + function _parseBase64Data() + { + $lines = Tests_Auth_OpenID_readlines('n2b64'); + + $data = array(); + foreach ($lines as $line) { + $line = trim($line); + if (!$line) { + continue; + } + list($b64, $ascii) = explode(' ', $line); + $data[$b64] = $ascii; + } + return $data; + } + + function _addB64Tests() + { + $lib =& Auth_OpenID_getMathLib(); + $count = defined('Tests_Auth_OpenID_thorough') ? -1 : 2; + $data = $this->_parseBase64Data(); + foreach ($data as $b64 => $num_s) { + // Only test the first few unless thorough is defined + if (strlen($num_s) > 5) { + if ($count == 0) { + break; + } else { + $count -= 1; + } + } + $num = $lib->init($num_s); + $test = new Tests_Auth_OpenID_Base64ToLong($lib, $b64, $num); + $test->setName("B64->Long $num_s"); + $this->addTest($test); + + $test = new Tests_Auth_OpenID_LongToBase64($lib, $b64, $num); + $test->setName("Long->B64 $num_s"); + $this->addTest($test); + } + } + + function _addBinLongTests() + { + $lib =& Auth_OpenID_getMathLib(); + $max = Tests_Auth_OpenID_maxint(); + $upper = defined('Tests_Auth_OpenID_thorough') ? 499 : 3; + + foreach (range(0, $upper) as $iteration) { + $test = new Tests_Auth_OpenID_BinLongConvertRnd($lib, $max); + $test->setName("BinLongConvertRnd " . strval($iteration)); + $this->addTest($test); + } + + $cases = array( + array("\x00", 0), + array("\x01", 1), + array("\x7F", 127), + array("\x00\x80", 128), + array("\x00\x81", 129), + array("\x00\xFF", 255), + array("\x00\x80\x00", 32768), + array("OpenID is cool", + "1611215304203901150134421257416556") + ); + + foreach ($cases as $case) { + list($bin, $lng_m) = $case; + $lng = $lib->init($lng_m); + $test = new Tests_Auth_OpenID_BinLongConvert($lib, $bin, $lng); + $test->setName('BinLongConvert ' . bin2hex($bin)); + $this->addTest($test); + } + + } + + function Tests_Auth_OpenID_BigMath($name) + { + $this->setName($name); + + if (!defined('Auth_OpenID_NO_MATH_SUPPORT')) { + $this->addTestSuite('Tests_Auth_OpenID_BigInt'); + $this->_addB64Tests(); + $this->_addBinLongTests(); + $test = new Tests_Auth_OpenID_Rand(Auth_OpenID_getMathLib()); + $test->setName('Big number rand'); + $this->addTest($test); + } + } +} + +?>
\ No newline at end of file diff --git a/Tests/Auth/OpenID/Consumer.php b/Tests/Auth/OpenID/Consumer.php new file mode 100644 index 0000000..a762a9d --- /dev/null +++ b/Tests/Auth/OpenID/Consumer.php @@ -0,0 +1,2532 @@ +<?php + +/** + * Tests for the OpenID consumer. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +@session_start(); + +require_once 'Auth/OpenID/CryptUtil.php'; +require_once 'Auth/Yadis/HTTPFetcher.php'; +require_once 'Auth/OpenID/DiffieHellman.php'; +require_once 'Auth/OpenID/FileStore.php'; +require_once 'Auth/OpenID/KVForm.php'; +require_once 'Auth/OpenID/Consumer.php'; +require_once 'Auth/OpenID/Server.php'; +require_once 'Auth/OpenID/Nonce.php'; +require_once 'Auth/OpenID/SReg.php'; +require_once 'Auth/OpenID/Message.php'; +require_once 'Tests/Auth/OpenID/MemStore.php'; +require_once 'PHPUnit.php'; + +/* + * Convenience function to create a SuccessResponse with the given + * arguments, all signed. + */ +function mkSuccess($endpoint, $q) +{ + $signed_list = array(); + foreach (array_keys($q) as $k) { + $signed_list[] = 'openid.' . $k; + } + return new Auth_OpenID_SuccessResponse($endpoint, + Auth_OpenID_Message::fromOpenIDArgs($q), + $signed_list); +} + +class FastConsumerSession extends Auth_OpenID_DiffieHellmanSHA1ConsumerSession { + function FastConsumerSession($dh = null) + { + if ($dh === null) { + $dh = new Auth_OpenID_DiffieHellman(100389557, 2); + } + + $this->dh = $dh; + } +} + +function setConsumerSession(&$con) +{ + $con->session_types = array('DH-SHA1' => 'FastConsumerSession'); +} + +global $_Auth_OpenID_assocs; +$_Auth_OpenID_assocs = array( + array('another 20-byte key.', 'Snarky'), + array(str_repeat("\x00", 20), 'Zeros'), + ); + +function Auth_OpenID_parse($qs) +{ + $result = array(); + $parts = explode("&", $qs); + foreach ($parts as $pair) { + list($key, $value) = explode("=", $pair, 2); + assert(!array_key_exists($key, $result)); + $result[$key] = urldecode($value); + } + return $result; +} + +function Auth_OpenID_associate($qs, $assoc_secret, $assoc_handle) +{ + $query_data = Auth_OpenID_parse($qs); + + assert($query_data['openid.mode'] == 'associate'); + assert($query_data['openid.assoc_type'] == 'HMAC-SHA1'); + + $reply_dict = array( + 'assoc_type' => 'HMAC-SHA1', + 'assoc_handle' => $assoc_handle, + 'expires_in' => '600', + ); + + if (defined('Auth_OpenID_NO_MATH_SUPPORT')) { + assert(count($query_data) == 2); + $message = Auth_OpenID_Message::fromPostArgs($query_data); + $session = Auth_OpenID_PlainTextServerSession::fromMessage($message); + } else { + assert((count($query_data) == 6) || (count($query_data) == 4)); + assert($query_data['openid.mode'] == 'associate'); + assert($query_data['openid.session_type'] == 'DH-SHA1'); + + $message = Auth_OpenID_Message::fromPostArgs($query_data); + + $session = Auth_OpenID_DiffieHellmanSHA1ServerSession::fromMessage($message); + $reply_dict['session_type'] = 'DH-SHA1'; + + } + + $reply_dict = array_merge($reply_dict, $session->answer($assoc_secret)); + return Auth_OpenID_KVForm::fromArray($reply_dict); +} + +class Auth_OpenID_TestFetcher extends Auth_Yadis_HTTPFetcher { + function Auth_OpenID_TestFetcher($user_url, $user_page, + $assoc_secret, $assoc_handle) + { + $this->get_responses = array($user_url => + new Auth_Yadis_HTTPResponse($user_url, + 200, + array(), + $user_page)); + $this->assoc_secret = $assoc_secret; + $this->assoc_handle = $assoc_handle; + $this->num_assocs = 0; + } + + function response($url, $body) + { + if ($body === null) { + return new Auth_Yadis_HTTPResponse($url, 404, array(), 'Not found'); + } else { + return new Auth_Yadis_HTTPResponse($url, 200, array(), $body); + } + } + + function get($url) + { + if (array_key_exists($url, $this->get_responses)) { + return $this->get_responses[$url]; + } else { + return $this->response($url, null); + } + } + + function _checkAuth($url, $body) + { + $query_data = Auth_OpenID_parse($body); + $expected = array( + 'openid.mode' => 'check_authentication', + 'openid.signed' => 'assoc_handle,sig,signed', + 'openid.sig' => 'fake', + 'openid.assoc_handle' => $this->assoc_handle, + ); + + if ($query_data == $expected) { + return new Auth_Yadis_HTTPResponse($url, 200, array(), "is_valid:true\n"); + } else { + return new Auth_Yadis_HTTPResponse($url, 400, array(), + "error:bad check_authentication query\n"); + } + } + + function post($url, $body) + { + if (strpos($body, 'openid.mode=associate') !== false) { + $response = Auth_OpenID_associate($body, $this->assoc_secret, + $this->assoc_handle); + $this->num_assocs++; + return $this->response($url, $response); + } elseif (strpos($body, 'openid.mode=check_authentication') !== false) { + return $this->_checkAuth($url, $body); + } + + return $this->response($url, null); + } +} + +global $_Auth_OpenID_user_page_pat; +$_Auth_OpenID_user_page_pat = "<html> + <head> + <title>A user page</title> + %s + </head> + <body> + blah blah + </body> +</html>"; + +global $_Auth_OpenID_server_url; +$_Auth_OpenID_server_url = "http://server.example.com/"; + +global $_Auth_OpenID_consumer_url; +$_Auth_OpenID_consumer_url = "http://consumer.example.com/"; + +class Tests_Auth_OpenID_Consumer extends PHPUnit_TestCase { + + function _run(&$consumer, $user_url, $mode, $delegate_url, + &$fetcher, &$store, $immediate) + { + global $_Auth_OpenID_consumer_url, + $_Auth_OpenID_server_url; + + if (!defined('Auth_OpenID_NO_MATH_SUPPORT')) { + setConsumerSession($consumer); + } + + $endpoint = new Auth_OpenID_ServiceEndpoint(); + $endpoint->claimed_id = $user_url; + $endpoint->server_url = $_Auth_OpenID_server_url; + $endpoint->local_id = $delegate_url; + $endpoint->type_uris = array(Auth_OpenID_TYPE_1_1); + + $result = $consumer->begin($endpoint); + + $return_to = $_Auth_OpenID_consumer_url; + $trust_root = $_Auth_OpenID_consumer_url; + $redirect_url = $result->redirectURL($trust_root, $return_to, + $immediate); + + $parsed = parse_url($redirect_url); + $qs = $parsed['query']; + $q = Auth_OpenID_parse($qs); + $new_return_to = $q['openid.return_to']; + unset($q['openid.return_to']); + + $expected = array( + 'openid.mode' => $mode, + 'openid.identity' => $delegate_url, + 'openid.trust_root' => $trust_root, + ); + + if ($consumer->_use_assocs) { + $expected['openid.assoc_handle'] = $fetcher->assoc_handle; + } + + $this->assertEquals($expected, $q); + $this->assertEquals(0, strpos($redirect_url, $_Auth_OpenID_server_url)); + $this->assertEquals(0, strpos($new_return_to, $return_to)); + + $parsed = parse_url($new_return_to); + $query = Auth_OpenID_parse($parsed['query']); + + $query = array_merge($query, array( + 'openid.mode'=> 'id_res', + 'openid.return_to'=> $new_return_to, + 'openid.identity'=> $delegate_url, + 'openid.assoc_handle'=> $fetcher->assoc_handle, + )); + + if (!$consumer->_use_assocs) { + $query['openid.signed'] = + 'assoc_handle,mode,signed,identity'; + $query['openid.assoc_handle'] = $fetcher->assoc_handle; + $query['openid.sig'] = 'fake'; + } + + $message = Auth_OpenID_Message::fromPostArgs($query); + + if ($consumer->_use_assocs) { + $assoc = $store->getAssociation($_Auth_OpenID_server_url, + $fetcher->assoc_handle); + $message = $assoc->signMessage($message); + } + + $result = $consumer->complete($message, $result->endpoint, $new_return_to); + + $this->assertEquals(Auth_OpenID_SUCCESS, $result->status); + $this->assertEquals($result->identity_url, $user_url); + } + + function _test_success($user_url, $delegate_url, $links, $immediate = false) + { + global $_Auth_OpenID_filestore_base_dir, + $_Auth_OpenID_server_url, + $_Auth_OpenID_user_page_pat, + $_Auth_OpenID_assocs; + + $store = new Tests_Auth_OpenID_MemStore(); + + if ($immediate) { + $mode = 'checkid_immediate'; + } else { + $mode = 'checkid_setup'; + } + + $user_page = sprintf($_Auth_OpenID_user_page_pat, $links); + $fetcher = new Auth_OpenID_TestFetcher($user_url, $user_page, + $_Auth_OpenID_assocs[0][0], + $_Auth_OpenID_assocs[0][1]); + + $consumer = new Auth_OpenID_GenericConsumer($store); + $consumer->fetcher =& $fetcher; + + $expected_num_assocs = 0; + $this->assertEquals($expected_num_assocs, $fetcher->num_assocs); + $this->_run($consumer, $user_url, $mode, $delegate_url, + $fetcher, $store, $immediate); + + if ($consumer->_use_assocs) { + $expected_num_assocs += 1; + } + + $this->assertEquals($expected_num_assocs, $fetcher->num_assocs); + + // Test that doing it again uses the existing association + $this->_run($consumer, $user_url, $mode, $delegate_url, + $fetcher, $store, $immediate); + + $this->assertEquals($expected_num_assocs, $fetcher->num_assocs); + + // Another association is created if we remove the existing one + $store->removeAssociation($_Auth_OpenID_server_url, + $fetcher->assoc_handle); + + $this->_run($consumer, $user_url, $mode, $delegate_url, + $fetcher, $store, $immediate); + + if ($consumer->_use_assocs) { + $expected_num_assocs += 1; + } + + $this->assertEquals($expected_num_assocs, $fetcher->num_assocs); + + // Test that doing it again uses the existing association + $this->_run($consumer, $user_url, $mode, $delegate_url, + $fetcher, $store, $immediate); + + $this->assertEquals($expected_num_assocs, $fetcher->num_assocs); + } + + function test_success() + { + global $_Auth_OpenID_server_url; + + $user_url = 'http://www.example.com/user.html'; + $links = sprintf('<link rel="openid.server" href="%s" />', + $_Auth_OpenID_server_url); + + $delegate_url = 'http://consumer.example.com/user'; + $delegate_links = sprintf('<link rel="openid.server" href="%s" />'. + '<link rel="openid.delegate" href="%s" />', + $_Auth_OpenID_server_url, $delegate_url); + + $this->_test_success($user_url, $user_url, $links); + $this->_test_success($user_url, $user_url, $links, true); + $this->_test_success($user_url, $delegate_url, $delegate_links); + $this->_test_success($user_url, $delegate_url, $delegate_links, true); + } +} + +class ConfigurableConsumer extends Auth_OpenID_GenericConsumer { + var $return_to_check_disabled = false; + + function disableReturnToChecking() { + $this->return_to_check_disabled = true; + } + + function complete($message, $endpoint, $return_to) { + if ($this->return_to_check_disabled) { + $return_to = null; + } + + return parent::complete($message, $endpoint, $return_to); + } + + function _checkReturnTo($unused, $unused2) { + if ($this->return_to_check_disabled) { + return true; + } else { + return parent::_checkReturnTo($unused, $unused2); + } + } +} + +class _TestIdRes extends PHPUnit_TestCase { + var $consumer_class = 'ConfigurableConsumer'; + + function setUp() + { + $this->store = new Tests_Auth_OpenID_MemStore(); + $cl = $this->consumer_class; + $this->consumer = new $cl($this->store); + $this->return_to = "http://some.host/path"; + $this->endpoint = new Auth_OpenID_ServiceEndpoint(); + + $this->server_id = "sirod"; + $this->server_url = "serlie"; + $this->consumer_id = "consu"; + + $this->endpoint->claimed_id = $this->consumer_id; + $this->endpoint->server_url = $this->server_url; + $this->endpoint->local_id = $this->server_id; + $this->endpoint->type_uris = array(Auth_OpenID_TYPE_1_1); + } +} + +class Tests_Auth_OpenID_Consumer_TestSetupNeeded extends _TestIdRes { + function failUnlessSetupNeeded($expected_setup_url, $message) + { + if ($this->consumer._checkSetupNeeded($message)) { + $this->assertEquals($expected_setup_url, + $message->getArg(Auth_OpenID_OPENID_NS, + 'user_setup_url')); + } else { + $this->fail("Expected to find an immediate-mode response"); + } + } + + function test_setupNeededOpenID1() + { + // The minimum conditions necessary to trigger Setup Needed + $setup_url = 'http://unittest/setup-here'; + $message = Auth_OpenID_Message::fromPostArgs(array( + 'opaenid.mode' => 'id_res', + 'openid.user_setup_url' => $setup_url + )); + $this->assertTrue($message->isOpenID1()); + $this->failUnlessSetupNeeded($setup_url, $message); + } + + function test_setupNeededOpenID1_extra() + { + // Extra stuff along with setup_url still trigger Setup Needed + $setup_url = 'http://unittest/setup-here'; + $message = Auth_OpenID_Message::fromPostArgs(array( + 'openid.mode' => 'id_res', + 'openid.user_setup_url' => $setup_url, + 'openid.identity' => 'bogus' + )); + $this->assertTrue($message->isOpenID1()); + $this->failUnlessSetupNeeded($setup_url, $message); + } + + function test_noSetupNeededOpenID1() + { + // When the user_setup_url is missing on an OpenID 1 message, + // we assume that it's not a cancel response to + // checkid_immediate + $message = Auth_OpenID_Message::fromOpenIDArgs(array('mode' => 'id_res')); + $this->assertTrue($message->isOpenID1()); + + // No SetupNeededError raised + $this->consumer->_checkSetupNeeded($message); + } + + function test_setupNeededOpenID2() + { + $message = Auth_OpenID_Message::fromOpenIDArgs(array( + 'mode' => 'setup_needed', + 'ns' => Auth_OpenID_OPENID2_NS + )); + $this->assertTrue($message->isOpenID2()); + $response = $this->consumer->complete($message, null, null); + $this->assertEquals('setup_needed', $response->status); + $this->assertEquals(null, $response->setup_url); + } + + function test_setupNeededDoesntWorkForOpenID1() + { + $message = Auth_OpenID_Message::fromOpenIDArgs(array( + 'mode' => 'setup_needed')); + + $this->assertFalse($this->consumer._checkSetupNeeded($message)); + + $response = $this->consumer->complete($message, null, null); + $this->assertEquals('failure', $response->status); + $this->assertTrue(strpos($response->message, 'Invalid openid.mode') === 0); + } + + function test_noSetupNeededOpenID2() + { + $message = Auth_OpenID_Message::fromOpenIDArgs(array( + 'mode' => 'id_res', + 'game' => 'puerto_rico', + 'ns' => Auth_OpenID_OPENID2_NS + )); + $this->assertTrue($message->isOpenID2()); + + $this->assertFalse($this->consumer._checkSetupNeeded($message)); + } +} + +class IdResCheckForFieldsTest extends _TestIdRes { + function setUp() { + # Argh. + $v = null; + $this->consumer = new Auth_OpenID_GenericConsumer($v); + } + + function successTest($openid_args, $signed_list) { + $message = Auth_OpenID_Message::fromOpenIDArgs($openid_args); + $message->setArg(Auth_OpenID_OPENID_NS, 'signed', implode(',', $signed_list)); + $result = $this->consumer->_idResCheckForFields($message); + $this->assertFalse(Auth_OpenID::isFailure($result)); + } + + function test_openid1Success() { + $this->successTest( + array('return_to' =>'return', + 'assoc_handle' =>'assoc handle', + 'sig' =>'a signature', + 'identity' =>'someone', + ), + array('return_to', 'identity')); + } + + function test_openid2Success() { + $this->successTest( + array('ns' => Auth_OpenID_OPENID2_NS, + 'return_to' =>'return', + 'assoc_handle' =>'assoc handle', + 'sig' =>'a signature', + 'op_endpoint' =>'my favourite server', + 'response_nonce' =>'use only once', + ), + array('return_to', 'response_nonce', 'assoc_handle')); + } + + function test_openid2Success_identifiers() { + $this->successTest( + array('ns' =>Auth_OpenID_OPENID2_NS, + 'return_to' =>'return', + 'assoc_handle' =>'assoc handle', + 'sig' =>'a signature', + 'claimed_id' =>'i claim to be me', + 'identity' =>'my server knows me as me', + 'op_endpoint' =>'my favourite server', + 'response_nonce' =>'use only once', + ), + array('return_to', 'response_nonce', 'identity', + 'claimed_id', 'assoc_handle')); + } + + function failureTest($openid_args, $signed_list) { + $message = Auth_OpenID_Message::fromOpenIDArgs($openid_args); + $result = $this->consumer->_idResCheckForFields($message); + $this->assertTrue(Auth_OpenID::isFailure($result)); + $this->assertTrue(strpos($result->message, 'Missing required') === 0); + } + + function test_openid1Missing_returnToSig() { + $this->failureTest( + array('return_to' =>'return', + 'assoc_handle' =>'assoc handle', + 'sig' =>'a signature', + 'identity' =>'someone', + ), + array('identity')); + } + + function test_openid1Missing_identitySig() { + $this->failureTest( + array('return_to' =>'return', + 'assoc_handle' =>'assoc handle', + 'sig' =>'a signature', + 'identity' =>'someone', + ), + array('return_to')); + } + + function test_openid1MissingReturnTo() { + $this->failureTest( + array('assoc_handle' =>'assoc handle', + 'sig' =>'a signature', + 'identity' =>'someone', + ), + array('return_to', 'identity')); + } + + function test_openid1MissingAssocHandle() { + $this->failureTest( + array('return_to' =>'return', + 'sig' =>'a signature', + 'identity' =>'someone', + ), + array('return_to', 'identity')); + } +} + +define('E_CHECK_AUTH_HAPPENED', 'checkauth occurred'); +define('E_MOCK_FETCHER_EXCEPTION', 'mock fetcher exception'); +define('E_ASSERTION_ERROR', 'assertion error'); + +class _CheckAuthDetectingConsumer extends ConfigurableConsumer { + function _verifyDiscoveryResults($message, $endpoint) + { + return $endpoint; + } + + function _idResCheckNonce($message, $endpoint) + { + return true; + } + + function _checkAuth($query, $server_url) + { + __raiseError(E_CHECK_AUTH_HAPPENED); + } +} + +global $GOODSIG; +$GOODSIG = "[A Good Signature]"; + +class GoodAssociation { + var $expiresIn = 3600; + var $handle = "-blah-"; + + function getExpiresIn() + { + return $this->expiresIn; + } + + function checkMessageSignature($message) + { + global $GOODSIG; + return $message->getArg(Auth_OpenID_OPENID_NS, 'sig') == $GOODSIG; + } +} + +class GoodAssocStore extends Tests_Auth_OpenID_MemStore { + function getAssociation($server_url, $handle = null) + { + return new GoodAssociation(); + } +} + +class TestIdResCheckSignature extends _TestIdRes { + function setUp() + { + global $GOODSIG; + + parent::setUp(); + $this->assoc = new GoodAssociation(); + $this->assoc->handle = "{not_dumb}"; + $this->store->storeAssociation($this->endpoint->server_url, $this->assoc); + + $this->message = Auth_OpenID_Message::fromPostArgs(array( + 'openid.mode'=> 'id_res', + 'openid.identity'=> '=example', + 'openid.sig'=> $GOODSIG, + 'openid.assoc_handle'=> $this->assoc->handle, + 'openid.signed'=> 'mode,identity,assoc_handle,signed', + 'frobboz'=> 'banzit')); + } + + function test_sign() + { + // assoc_handle to assoc with good sig + $this->consumer->_idResCheckSignature($this->message, + $this->endpoint->server_url); + } + + function test_signFailsWithBadSig() + { + $this->message.setArg(Auth_OpenID_OPENID_NS, 'sig', 'BAD SIGNATURE'); + $result = $this->consumer->_idResCheckSignature($this->message, $this->endpoint->server_url); + $this->assertTrue(Auth_OpenID::isFailure($result)); + } +} + +class StatelessConsumer1 extends ConfigurableConsumer { + function _processCheckAuthResponse($response, $server_url) + { + return true; + } + + function _makeKVPost($args, $server_url) + { + return array(); + } +} + +class Tests_Auth_OpenID_Stateless1 extends _TestIdRes { + var $consumer_class = "StatelessConsumer1"; + + function setUp() + { + global $GOODSIG; + + parent::setUp(); + $this->assoc = new GoodAssociation(); + $this->assoc->handle = "{not_dumb}"; + $this->store->storeAssociation($this->endpoint->server_url, $this->assoc); + + $this->message = Auth_OpenID_Message::fromPostArgs(array( + 'openid.mode'=> 'id_res', + 'openid.identity'=> '=example', + 'openid.sig'=> $GOODSIG, + 'openid.assoc_handle'=> $this->assoc->handle, + 'openid.signed'=> 'mode,identity,assoc_handle,signed', + 'frobboz'=> 'banzit')); + } + + function test_stateless() + { + // assoc_handle missing assoc, consumer._checkAuth returns + // goodthings + $this->message->setArg(Auth_OpenID_OPENID_NS, "assoc_handle", "dumbHandle"); + $this->consumer->_idResCheckSignature($this->message, + $this->endpoint->server_url); + } +} + +class StatelessConsumer2 extends ConfigurableConsumer { + function _checkAuth($_, $__) + { + return false; + } +} + +class Tests_Auth_OpenID_Stateless2 extends _TestIdRes { + var $consumer_class = "StatelessConsumer2"; + + function setUp() + { + global $GOODSIG; + + parent::setUp(); + $this->assoc = new GoodAssociation(); + $this->assoc->handle = "{not_dumb}"; + $this->store->storeAssociation($this->endpoint->server_url, $this->assoc); + + $this->message = Auth_OpenID_Message::fromPostArgs(array( + 'openid.mode'=> 'id_res', + 'openid.identity'=> '=example', + 'openid.sig'=> $GOODSIG, + 'openid.assoc_handle'=> $this->assoc->handle, + 'openid.signed'=> 'mode,identity,assoc_handle,signed', + 'frobboz'=> 'banzit')); + } + + function test_statelessRaisesError() + { + // assoc_handle missing assoc, consumer._checkAuth returns + // goodthings + $this->message->setArg(Auth_OpenID_OPENID_NS, "assoc_handle", + "dumbHandle"); + $result = $this->consumer->_idResCheckSignature($this->message, + $this->endpoint->server_url); + $this->assertTrue(Auth_OpenID::isFailure($result)); + } +} + +class Tests_Auth_OpenID_Consumer_CheckNonceTest extends _TestIdRes { + function setUp() + { + parent::setUp(); + $this->consumer->openid1_nonce_query_arg_name = 'nonce'; + } + + function test_openid1Success() + { + // use consumer-generated nonce + $nonce_value = Auth_OpenID_mkNonce(); + $this->return_to = sprintf('http://rt.unittest/?nonce=%s', + $nonce_value); + $this->response = Auth_OpenID_Message::fromOpenIDArgs( + array('return_to' => $this->return_to)); + $this->response->setArg(Auth_OpenID_BARE_NS, 'nonce', $nonce_value); + + $result = $this->consumer->_idResCheckNonce($this->response, $this->endpoint); + $this->assertFalse(Auth_OpenID::isFailure($result)); + } + + function test_openid1Missing() + { + // use consumer-generated nonce + $this->response = Auth_OpenID_Message::fromOpenIDArgs(array()); + $n = $this->consumer->_idResGetNonceOpenID1($this->response, $this->endpoint); + $this->assertTrue($n === null); + } + + function test_consumerNonceOpenID2() + { + // OpenID 2 does not use consumer-generated nonce + $this->return_to = sprintf('http://rt.unittest/?nonce=%s', + Auth_OpenID_mkNonce()); + $this->response = Auth_OpenID_Message::fromOpenIDArgs( + array('return_to' => $this->return_to, + 'ns' => Auth_OpenID_OPENID2_NS)); + $result = $this->consumer->_idResCheckNonce($this->response, $this->endpoint); + $this->assertTrue(Auth_OpenID::isFailure($result)); + } + + function test_serverNonce() + { + // use server-generated nonce + $this->response = Auth_OpenID_Message::fromOpenIDArgs( + array('ns' => Auth_OpenID_OPENID2_NS, + 'response_nonce' => Auth_OpenID_mkNonce())); + $this->consumer->_idResCheckNonce($this->response, $this->endpoint); + } + + function test_serverNonceOpenID1() + { + // OpenID 1 does not use server-generated nonce + $this->response = Auth_OpenID_Message::fromOpenIDArgs( + array('ns' => Auth_OpenID_OPENID1_NS, + 'return_to'=> 'http://return.to/', + 'response_nonce'=> Auth_OpenID_mkNonce())); + $result = $this->consumer->_idResCheckNonce($this->response, $this->endpoint); + $this->assertTrue(Auth_OpenID::isFailure($result)); + } + + function test_badNonce() + { + // remove the nonce from the store + $nonce = Auth_OpenID_mkNonce(); + list($timestamp, $salt) = Auth_OpenID_splitNonce($nonce); + + $this->store->useNonce($this->server_url, $timestamp, $salt); + + $response = Auth_OpenID_Message::fromOpenIDArgs(array( + 'response_nonce' => $nonce, + 'ns' => Auth_OpenID_OPENID2_NS + )); + + $result = $this->consumer->_idResCheckNonce($response, + $this->endpoint); + + $this->assertTrue(Auth_OpenID::isFailure($result)); + } + + function test_tamperedNonce() + { + // Malformed nonce + $query = array('response_nonce' => 'malformed', + 'ns' => Auth_OpenID_OPENID2_NS); + $message = Auth_OpenID_Message::fromPostArgs($query); + + $result = $this->consumer->_idResCheckNonce($message, + $this->endpoint); + + $this->assertTrue(Auth_OpenID::isFailure($result)); + } + + function test_missingNonce() + { + // no nonce parameter on the return_to + $query = array('openid.return_to' => $this->return_to); + $message = Auth_OpenID_Message::fromPostArgs($query); + + $result = $this->consumer->_idResCheckNonce($message, + $this->endpoint); + + $this->assertTrue(Auth_OpenID::isFailure($result)); + } +} + +class Tests_Auth_OpenID_Consumer_TestCheckAuthTriggered extends _TestIdRes { + var $consumer_class = '_CheckAuthDetectingConsumer'; + + function _doIdRes($message, $endpoint, $return_to) + { + return $this->consumer->_doIdRes($message, $endpoint, $return_to); + } + + function test_checkAuthTriggered() + { + $query = array('openid.return_to' => $this->return_to, + 'openid.identity' => $this->server_id, + 'openid.assoc_handle' =>'not_found', + 'openid.sig' => 'bogus', + 'openid.signed' => 'identity,return_to'); + + $message = Auth_OpenID_Message::fromPostArgs($query); + + $this->consumer->disableReturnToChecking(); + + $result = $this->_doIdRes($message, $this->endpoint, null); + + $error = __getError(); + + if ($error === null) { + $this->fail('_checkAuth did not happen.'); + } + } + + function test_checkAuthTriggeredWithAssoc() + { + // Store an association for this server that does not match + // the handle that is in the query + $issued = time(); + $lifetime = 1000; + $assoc = new Auth_OpenID_Association( + 'handle', 'secret', $issued, $lifetime, 'HMAC-SHA1'); + $this->store->storeAssociation($this->server_url, $assoc); + + $query = array( + 'openid.return_to' => $this->return_to, + 'openid.identity' => $this->server_id, + 'openid.assoc_handle' => 'not_found', + 'openid.sig' => 'bogus', + 'openid.signed' => 'return_to,identity'); + + $this->consumer->disableReturnToChecking(); + + $message = Auth_OpenID_Message::fromPostArgs($query); + + $result = $this->_doIdRes($message, $this->endpoint, null); + $error = __getError(); + + if ($error === null) { + $this->fail('_checkAuth did not happen.'); + } + } + + function test_expiredAssoc() + { + // Store an expired association for the server with the handle + // that is in the query + $issued = time() - 10; + $lifetime = 0; + $handle = 'handle'; + $assoc = new Auth_OpenID_Association( + $handle, 'secret', $issued, $lifetime, 'HMAC-SHA1'); + $this->assertTrue($assoc->getExpiresIn() <= 0); + $this->store->storeAssociation($this->server_url, $assoc); + + $query = array( + 'openid.return_to' => $this->return_to, + 'openid.identity' => $this->server_id, + 'openid.sig' => 'bogus', + 'openid.signed' => 'identity,return_to', + 'openid.assoc_handle' => $handle); + + $message = Auth_OpenID_Message::fromPostArgs($query); + + $this->consumer->disableReturnToChecking(); + + $info = $this->_doIdRes($message, $this->endpoint, null); + + $this->assertEquals('failure', $info->status); + + $this->assertTrue(strpos($info->message, 'expired') !== false); + } + + function test_newerAssoc() + { + // Store an expired association for the server with the handle + // that is in the query + $lifetime = 1000; + + $good_issued = time() - 10; + $good_handle = 'handle'; + $good_assoc = new Auth_OpenID_Association( + $good_handle, 'secret', $good_issued, $lifetime, 'HMAC-SHA1'); + $this->store->storeAssociation($this->server_url, $good_assoc); + + $bad_issued = time() - 5; + $bad_handle = 'handle2'; + $bad_assoc = new Auth_OpenID_Association( + $bad_handle, 'secret', $bad_issued, $lifetime, 'HMAC-SHA1'); + $this->store->storeAssociation($this->server_url, $bad_assoc); + + $query = array( + 'openid.return_to' => $this->return_to, + 'openid.identity' => $this->server_id, + 'openid.assoc_handle' => $good_handle); + + $this->consumer->disableReturnToChecking(); + + $message = Auth_OpenID_Message::fromPostArgs($query); + $message = $good_assoc->signMessage($message); + + $info = $this->_doIdRes($message, $this->endpoint, null); + + $this->assertEquals($info->status, 'success'); + $this->assertEquals($this->consumer_id, $info->identity_url); + } +} + +class _MockFetcher { + function _MockFetcher($response = null) + { + // response is (code, url, body) + $this->response = $response; + $this->fetches = array(); + } + + function post($url, $body) + { + $this->fetches[] = array($url, $body, array()); + return $this->response; + } + + function get($url) + { + $this->fetches[] = array($url, null, array()); + return $this->response; + } +} + +class Tests_Auth_OpenID_Complete extends _TestIdRes { + function test_cancel() + { + $query = array('openid.mode' => 'cancel'); + $message = Auth_OpenID_Message::fromPostArgs($query); + + $this->consumer->disableReturnToChecking(); + + $r = $this->consumer->complete($message, $this->endpoint, null); + $this->assertEquals($r->status, Auth_OpenID_CANCEL); + $this->assertTrue($r->identity_url == $this->endpoint->claimed_id); + } + + function test_cancel_with_return_to() { + $message = Auth_OpenID_Message::fromPostArgs(array('openid.mode' => 'cancel')); + $r = $this->consumer->complete($message, $this->endpoint, $this->return_to); + $this->assertEquals($r->status, Auth_OpenID_CANCEL); + $this->assertTrue($r->identity_url == $this->endpoint->claimed_id); + } + + function test_errorWithNoOptionalKeys() + { + $msg = 'an error message'; + $contact = 'some contact info here'; + $message = Auth_OpenID_Message::fromPostArgs(array('openid.mode'=> 'error', + 'openid.error'=> $msg, + 'openid.contact'=> $contact)); + + $this->consumer->disableReturnToChecking(); + + $r = $this->consumer->complete($message, $this->endpoint, null); + $this->assertEquals($r->status, Auth_OpenID_FAILURE); + $this->assertTrue($r->identity_url == $this->endpoint->claimed_id); + $this->assertTrue($r->contact == $contact); + $this->assertTrue($r->reference === null); + $this->assertEquals($r->message, $msg); + } + + function test_errorWithOptionalKeys() + { + $msg = 'an error message'; + $contact = 'me'; + $reference = 'support ticket'; + $message = Auth_OpenID_Message::fromPostArgs(array('openid.mode'=> 'error', + 'openid.error'=> $msg, 'openid.reference'=> $reference, + 'openid.contact'=> $contact, 'openid.ns'=> Auth_OpenID_OPENID2_NS + )); + $r = $this->consumer->complete($message, $this->endpoint, null); + $this->assertEquals($r->status, Auth_OpenID_FAILURE); + $this->assertTrue($r->identity_url == $this->endpoint->claimed_id); + $this->assertTrue($r->contact == $contact); + $this->assertTrue($r->reference == $reference); + $this->assertEquals($r->message, $msg); + } + + function test_error() + { + $msg = 'an error message'; + $query = array('openid.mode' =>'error', + 'openid.error' => $msg); + $message = Auth_OpenID_Message::fromPostArgs($query); + + $this->consumer->disableReturnToChecking(); + + $r = $this->consumer->complete($message, $this->endpoint, null); + $this->assertEquals($r->status, Auth_OpenID_FAILURE); + $this->assertTrue($r->identity_url == $this->endpoint->claimed_id); + $this->assertEquals($r->message, $msg); + } + + function test_noMode() + { + $query = array(); + $message = Auth_OpenID_Message::fromPostArgs($query); + $r = $this->consumer->complete($message, $this->endpoint, null); + $this->assertEquals($r->status, Auth_OpenID_FAILURE); + $this->assertTrue($r->identity_url == $this->endpoint->claimed_id); + } + + function test_idResMissingField() + { + $query = array('openid.mode' => 'id_res'); + $message = Auth_OpenID_Message::fromPostArgs($query); + $r = $this->consumer->complete($message, $this->endpoint, null); + $this->assertTrue(Auth_openID::isFailure($r)); + } +} + +class _VerifiedError extends Auth_OpenID_FailureResponse { +} + +class Consumer_idResURLMismatch extends ConfigurableConsumer { + function _discoverAndVerify($to_match) + { + return new _VerifiedError(null, 'verified error'); + } +} + +class Tests_idResURLMismatch extends _TestIdRes { + var $consumer_class = 'Consumer_idResURLMismatch'; + + function test_idResURLMismatch() + { + $query = array('openid.mode' => 'id_res', + 'openid.return_to' => 'return_to (just anything)', + 'openid.identity' => 'something wrong (not this->consumer_id)', + 'openid.assoc_handle' => 'does not matter', + 'openid.signed' => 'identity,return_to', + 'openid.sig' => 'bogus'); + + $this->consumer->disableReturnToChecking(); + + $message = Auth_OpenID_Message::fromPostArgs($query); + $r = $this->consumer->complete($message, $this->endpoint, null); + $this->assertTrue(is_a($r, '_VerifiedError')); + } +} + +class SetupNeededConsumer extends Auth_OpenID_GenericConsumer { + function _checkSetupNeeded($message) + { + return true; + } +} + +class Tests_Auth_OpenID_SetupNeeded extends _TestIdRes { + function test_setupNeededIdRes() + { + $message = Auth_OpenID_Message::fromOpenIDArgs(array('mode'=> 'id_res')); + $response = $this->consumer->complete($message, null, null); + $this->assertEquals(Auth_OpenID_SETUP_NEEDED, + $response->status); + } +} + +class TempConsumer extends ConfigurableConsumer { + function _verifyDiscoveryResults($message, $endpoint) + { + return $endpoint; + } +} + +class TestCompleteMissingSig extends PHPUnit_TestCase { + + function setUp() + { + global $GOODSIG; + + $this->store = new GoodAssocStore(); + $this->consumer = new ConfigurableConsumer($this->store); + $this->server_url = "http://idp.unittest/"; + + $claimed_id = 'bogus.claimed'; + + $this->message = Auth_OpenID_Message::fromOpenIDArgs( + array('mode'=> 'id_res', + 'return_to'=> 'return_to (just anything)', + 'identity'=> $claimed_id, + 'assoc_handle'=> 'does not matter', + 'sig'=> $GOODSIG, + 'response_nonce'=> Auth_OpenID_mkNonce(), + 'signed'=> 'identity,return_to,response_nonce,assoc_handle,claimed_id', + 'claimed_id'=> $claimed_id, + 'op_endpoint'=> $this->server_url, + 'ns' => Auth_OpenID_OPENID2_NS)); + + $this->endpoint = new Auth_OpenID_ServiceEndpoint(); + $this->endpoint->server_url = $this->server_url; + $this->endpoint->claimed_id = $claimed_id; + $this->consumer->disableReturnToChecking(); + } + + function test_idResMissingNoSigs() + { + $c = new TempConsumer($this->store); + $c->disableReturnToChecking(); + $r = $c->complete($this->message, $this->endpoint, null); + $this->failUnlessSuccess($r); + } + + function test_idResNoIdentity() + { + $this->message->delArg(Auth_OpenID_OPENID_NS, 'identity'); + $this->message->delArg(Auth_OpenID_OPENID_NS, 'claimed_id'); + $this->endpoint->claimed_id = null; + $this->message->setArg(Auth_OpenID_OPENID_NS, + 'signed', 'return_to,response_nonce,assoc_handle'); + $r = $this->consumer->complete($this->message, $this->endpoint, null); + $this->failUnlessSuccess($r); + } + + function test_idResMissingIdentitySig() + { + $this->message->setArg(Auth_OpenID_OPENID_NS, + 'signed', + 'return_to,response_nonce,assoc_handle,claimed_id'); + $r = $this->consumer->complete($this->message, $this->endpoint, null); + $this->assertEquals($r->status, Auth_OpenID_FAILURE); + } + + function test_idResMissingReturnToSig() + { + $this->message->setArg(Auth_OpenID_OPENID_NS, + 'signed', + 'identity,response_nonce,assoc_handle,claimed_id'); + $r = $this->consumer->complete($this->message, $this->endpoint, null); + $this->assertEquals($r->status, Auth_OpenID_FAILURE); + } + + function test_idResMissingAssocHandleSig() + { + $this->message->setArg(Auth_OpenID_OPENID_NS, 'signed', + 'identity,response_nonce,return_to,claimed_id'); + $r = $this->consumer->complete($this->message, $this->endpoint, null); + $this->assertEquals($r->status, Auth_OpenID_FAILURE); + } + + function test_idResMissingClaimedIDSig() + { + $this->message->setArg(Auth_OpenID_OPENID_NS, 'signed', + 'identity,response_nonce,return_to,assoc_handle'); + $r = $this->consumer->complete($this->message, $this->endpoint, null); + $this->assertEquals($r->status, Auth_OpenID_FAILURE); + } + + function failUnlessSuccess($response) + { + if ($response->status != Auth_OpenID_SUCCESS) { + $this->fail(sprintf("Non-successful response: %s", $response->status)); + } + } +} + +class TestReturnToArgs extends PHPUnit_TestCase { + function setUp() + { + $store = null; + $this->consumer = new Auth_OpenID_GenericConsumer($store); + } + + function test_returnToArgsUnexpectedArg() + { + $query = array( + 'openid.mode' => 'id_res', + 'openid.return_to' => 'http://example.com/', + 'foo' => 'bar'); + + // no return value, success is assumed if there are no + // exceptions. + $this->assertTrue(Auth_OpenID::isFailure( + $this->consumer->_verifyReturnToArgs($query))); + } + + function test_returnToArgsOkay() + { + $query = array( + 'openid.mode'=> 'id_res', + 'openid.return_to'=> 'http://example.com/?foo=bar', + 'foo'=> 'bar' + ); + // no return value, success is assumed if there are no exceptions. + $result = $this->consumer->_verifyReturnToArgs($query); + + $this->assertFalse(Auth_OpenID::isFailure($result)); + $this->assertTrue($result); + } + + function test_returnToMismatch() + { + $query = array( + 'openid.mode' => 'id_res', + 'openid.return_to' => 'http://example.com/?foo=bar'); + + // fail, query has no key 'foo'. + $result = $this->consumer->_verifyReturnToArgs($query); + $this->assertTrue(Auth_OpenID::isFailure($result)); + + $query['foo'] = 'baz'; + // fail, values for 'foo' do not match. + $result = $this->consumer->_verifyReturnToArgs($query); + $this->assertTrue(Auth_OpenID::isFailure($result)); + } + + function test_noReturnTo() + { + $query = array('openid.mode'=> 'id_res'); + $result = $this->consumer->_verifyReturnToArgs($query); + $this->assertTrue(Auth_OpenID::isFailure($result)); + } + + function test_completeBadReturnTo() + { + // Test GenericConsumer.complete()'s handling of bad return_to + // values. + $return_to = "http://some.url/path?foo=bar"; + + // Scheme, authority, and path differences are checked by + // GenericConsumer._checkReturnTo. Query args checked by + // GenericConsumer._verifyReturnToArgs. + $bad_return_tos = array( + // Scheme only + "https://some.url/path?foo=bar", + // Authority only + "http://some.url.invalid/path?foo=bar", + // Path only + "http://some.url/path_extra?foo=bar", + // Query args differ + "http://some.url/path?foo=bar2", + "http://some.url/path?foo2=bar" + ); + + $m = new Auth_OpenID_Message(Auth_OpenID_OPENID1_NS); + $m->setArg(Auth_OpenID_OPENID_NS, 'mode', 'cancel'); + $m->setArg(Auth_OpenID_BARE_NS, 'foo', 'bar'); + $endpoint = null; + + foreach ($bad_return_tos as $bad) { + $m->setArg(Auth_OpenID_OPENID_NS, 'return_to', $bad); + $this->assertFalse($this->consumer->_checkReturnTo($m, $return_to)); + } + } + + function test_completeGoodReturnTo() + { + // Test GenericConsumer.complete()'s handling of good + // return_to values. + $return_to = "http://some.url/path"; + + $good_return_tos = array( + array($return_to, + array()), + array($return_to . "?another=arg", + array(array( + array(Auth_OpenID_BARE_NS, 'another'), + 'arg'))), + array($return_to . "?another=arg#fragment", + array(array( + array(Auth_OpenID_BARE_NS, 'another'), + 'arg'))), + array("HTTP://some.url/path",array()), + array("http://some.URL/path",array()), + array("http://some.url:80/path",array()), + array("http://some.url/./path",array()) + ); + + $endpoint = null; + + foreach ($good_return_tos as $pair) { + list($good_return_to, $extra) = $pair; + $m = new Auth_OpenID_Message(Auth_OpenID_OPENID1_NS); + $m->setArg(Auth_OpenID_OPENID_NS, 'mode', 'cancel'); + + for ($i = 0; $i < count($extra); $i++) { + list($ckey, $value) = $extra[$i]; + $ns = $ckey[0]; + $key = $ckey[1]; + $m->setArg($ns, $key, $value); + } + + $m->setArg(Auth_OpenID_OPENID_NS, 'return_to', $good_return_to); + $result = $this->consumer->complete($m, $endpoint, $return_to); + $this->assertTrue(is_a($result, 'Auth_OpenID_CancelResponse')); + } + } +} + +class Tests_Auth_OpenID_CheckAuthResponse extends _TestIdRes { + function _createAssoc() + { + $issued = time(); + $lifetime = 1000; + $assoc = new Auth_OpenID_Association( + 'handle', 'secret', $issued, $lifetime, 'HMAC-SHA1'); + $store =& $this->consumer->store; + $store->storeAssociation($this->server_url, $assoc); + $assoc2 = $store->getAssociation($this->server_url); + $this->assertEquals($assoc, $assoc2); + } + + function test_goodResponse() + { + // successful response to check_authentication + $response = array('is_valid' => 'true'); + $message = Auth_OpenID_Message::fromOpenIDArgs($response); + $r = $this->consumer->_processCheckAuthResponse($message, $this->server_url); + $this->assertTrue($r); + } + + function test_missingAnswer() + { + // check_authentication returns false when the server sends no + // answer + $response = array(); + $message = Auth_OpenID_Message::fromPostArgs($response); + $r = $this->consumer->_processCheckAuthResponse($message, $this->server_url); + $this->assertFalse($r); + } + + function test_badResponse() + { + // check_authentication returns false when is_valid is false + $response = array('is_valid' => 'false'); + $message = Auth_OpenID_Message::fromOpenIDArgs($response); + + $r = $this->consumer->_processCheckAuthResponse($message, $this->server_url); + $this->assertFalse($r); + } + + function test_badResponseInvalidate() + { + // Make sure that the handle is invalidated when is_valid is + // false + $this->_createAssoc(); + $response = array('is_valid' => 'false', + 'invalidate_handle' => 'handle'); + + $message = Auth_OpenID_Message::fromOpenIDArgs($response); + + $r = $this->consumer->_processCheckAuthResponse($message, + $this->server_url); + $this->assertFalse($r); + $this->assertTrue( + $this->consumer->store->getAssociation($this->server_url) === null); + } + + function test_invalidateMissing() + { + // invalidate_handle with a handle that is not present + $response = array('is_valid' => 'true', + 'invalidate_handle' => 'missing'); + + $message = Auth_OpenID_Message::fromOpenIDArgs($response); + + $r = $this->consumer->_processCheckAuthResponse($message, $this->server_url); + $this->assertTrue($r); + } + + function test_invalidatePresent() + { + // invalidate_handle with a handle that exists""" + $this->_createAssoc(); + $response = array('is_valid' => 'true', + 'invalidate_handle' => 'handle'); + + $message = Auth_OpenID_Message::fromOpenIDArgs($response); + + $r = $this->consumer->_processCheckAuthResponse($message, $this->server_url); + $this->assertTrue($r); + $this->assertTrue( + $this->consumer->store->getAssociation($this->server_url) === null); + } +} + +class _IdResFetchFailingConsumer extends Auth_OpenID_GenericConsumer { + var $message = 'fetch failed'; + + function _doIdRes($message, $endpoint) + { + return new Auth_OpenID_FailureResponse($endpoint, + $this->message); + } +} + +class Tests_Auth_OpenID_FetchErrorInIdRes extends _TestIdRes { + var $consumer_class = '_IdResFetchFailingConsumer'; + + function test_idResFailure() + { + $query = array('openid.mode' => 'id_res'); + $message = Auth_OpenID_Message::fromPostArgs($query); + $r = $this->consumer->complete($message, $this->endpoint, null); + $this->assertEquals($r->status, Auth_OpenID_FAILURE); + $this->assertEquals($r->identity_url, $this->consumer_id); + $this->assertEquals($this->consumer->message, $r->message); + } +} + +class _ExceptionRaisingMockFetcher { + function get($url) + { + __raiseError(E_MOCK_FETCHER_EXCEPTION); + } + + function post($url, $body) + { + __raiseError(E_MOCK_FETCHER_EXCEPTION); + + return new Auth_Yadis_HTTPResponse($url, 400, + array(), ''); + } +} + +class _BadArgCheckingConsumer extends Auth_OpenID_GenericConsumer { + function _makeKVPost($message, $tmp) + { + $args = $message->toPostArgs(); + + if ($args != array( + 'openid.mode' => 'check_authentication', + 'openid.signed' => 'foo')) { + __raiseError(E_ASSERTION_ERROR); + } + return null; + } +} + +class Tests_Auth_OpenID_Consumer_TestCheckAuth extends _TestIdRes { + function setUp() + { + $this->store = new Tests_Auth_OpenID_MemStore(); + $this->consumer = new Auth_OpenID_GenericConsumer($this->store); + $this->fetcher = new _MockFetcher(); + $this->consumer->fetcher =& $this->fetcher; + } + + function test_checkauth_error() + { + global $_Auth_OpenID_server_url; + $this->fetcher->response = new Auth_Yadis_HTTPResponse("http://some_url", + 404, + array(), + "blah:blah\n"); + $query = array('openid.signed' => 'stuff, things'); + $message = Auth_OpenID_Message::fromPostArgs($query); + + $r = $this->consumer->_checkAuth($message, $_Auth_OpenID_server_url); + if ($r !== false) { + $this->fail("Expected _checkAuth result to be false"); + } + } + + function test_bad_args() + { + $query = array('openid.signed' => 'foo', + 'closid.foo' => 'something'); + + $consumer = new _BadArgCheckingConsumer($this->store); + + $message = Auth_OpenID_Message::fromPostArgs($query); + + $this->assertFalse($consumer->_checkAuth($message, 'does://not.matter')); + } + + function test_signedList() + { + $signed = 'identity,mode,ns.sreg,sreg.email'; + $query = Auth_OpenID_Message::fromOpenIDArgs(array( + 'mode'=> 'id_res', + 'sig'=> 'rabbits', + 'identity'=> '=example', + 'assoc_handle'=> 'munchkins', + 'ns.sreg' => 'urn:sreg', + 'sreg.email' => 'bogus@example.com', + 'signed'=> $signed, + 'foo'=> 'bar')); + + $args = $this->consumer->_createCheckAuthRequest($query); + $this->assertTrue($args->isOpenID1()); + $signed_list = explode(',',$signed); + foreach ($signed_list as $k) { + $this->assertTrue($args->getAliasedArg($k)); + } + } + + + function test_112() + { + $args = array('openid.assoc_handle' => 'fa1f5ff0-cde4-11dc-a183-3714bfd55ca8', + 'openid.claimed_id' => 'http://binkley.lan/user/test01', + 'openid.identity' => 'http://test01.binkley.lan/', + 'openid.mode' => 'id_res', + 'openid.ns' => 'http://specs.openid.net/auth/2.0', + 'openid.ns.pape' => 'http://specs.openid.net/extensions/pape/1.0', + 'openid.op_endpoint' => 'http://binkley.lan/server', + 'openid.pape.auth_policies' => 'none', + 'openid.pape.auth_time' => '2008-01-28T20 =>42 =>36Z', + 'openid.pape.nist_auth_level' => '0', + 'openid.response_nonce' => '2008-01-28T21 =>07 =>04Z99Q=', + 'openid.return_to' => 'http://binkley.lan =>8001/process?janrain_nonce=2008-01-28T21%3A07%3A02Z0tMIKx', + 'openid.sig' => 'YJlWH4U6SroB1HoPkmEKx9AyGGg=', + 'openid.signed' => 'assoc_handle,identity,response_nonce,return_to,claimed_id,op_endpoint,pape.auth_time,ns.pape,pape.nist_auth_level,pape.auth_policies' + ); + $this->assertEquals(Auth_OpenID_OPENID2_NS, $args['openid.ns']); + $incoming = Auth_OpenID_Message::fromPostArgs($args); + $this->assertTrue($incoming->isOpenID2()); + $car = $this->consumer->_createCheckAuthRequest($incoming); + $expected_args = $args; + $expected_args['openid.mode'] = 'check_authentication'; + $expected = Auth_OpenID_Message::fromPostArgs($expected_args); + $this->assertTrue($expected->isOpenID2()); + $this->assertEquals($expected, $car); + $this->assertEquals($expected_args, $car->toPostArgs()); + } +} + +class Tests_Auth_OpenID_Consumer_TestFetchAssoc extends PHPUnit_TestCase { + function setUp() + { + $this->store = new Tests_Auth_OpenID_MemStore(); + $this->fetcher = new _MockFetcher(); + $this->consumer = new Auth_OpenID_GenericConsumer($this->store); + $this->consumer->fetcher =& $this->fetcher; + } + + function test_kvpost_error() + { + $this->fetcher->response = new Auth_Yadis_HTTPResponse("http://some_url", + 404, + array(), + "blah:blah\n"); + $query = array('openid.mode' => 'associate'); + $message = Auth_OpenID_Message::fromPostArgs($query); + + $r = $this->consumer->_makeKVPost($message, + "http://server_url"); + if ($r !== null) { + $this->fail("Expected _makeKVPost result to be null"); + } + } + + function test_error_404() + { + // 404 from a kv post raises HTTPFetchingError + $this->fetcher->response = new Auth_Yadis_HTTPResponse( + "http://some_url", 404, array('Hea'=> 'der'), 'blah:blah\n'); + + $result = $this->consumer->_makeKVPost( + Auth_OpenID_Message::fromPostArgs(array('mode'=>'associate')), + "http://server_url"); + + $this->assertTrue($result === null); + } + + function test_error_exception() + { + $this->consumer->fetcher = new _ExceptionRaisingMockFetcher(); + + $query = array('openid.mode' => 'associate'); + $message = Auth_OpenID_Message::fromPostArgs($query); + + $this->consumer->_makeKVPost($message, + "http://server_url"); + + if (__getError() !== E_MOCK_FETCHER_EXCEPTION) { + $this->fail("Expected ExceptionRaisingMockFetcher to " . + "raise E_MOCK_FETCHER_EXCEPTION"); + } + + $endpoint = new Auth_OpenID_ServiceEndpoint(); + $endpoint->server_url = 'some://url'; + + // exception fetching returns no association + $this->assertEquals($this->consumer->_getAssociation($endpoint), + null); + + $query = array('openid.signed' => ''); + $message = Auth_OpenID_Message::fromPostArgs($query); + + $this->consumer->_checkAuth($message, + 'some://url'); + + if (__getError() !== E_MOCK_FETCHER_EXCEPTION) { + $this->fail("Expected ExceptionRaisingMockFetcher to " . + "raise E_MOCK_FETCHER_EXCEPTION (_checkAuth)"); + } + } +} + +class Tests_Auth_OpenID_AuthRequestHTMLMarkup extends PHPUnit_TestCase { + function setUp() + { + $this->endpoint = new Auth_OpenID_ServiceEndpoint(); + $this->endpoint->claimed_id = 'identity_url'; + + $this->request = new Auth_OpenID_AuthRequest($this->endpoint, null); + } + + function test_htmlMarkup() + { + $html = $this->request->htmlMarkup('http://realm.com/', + 'http://realm.com/return_to'); + $this->assertTrue(substr($html,"<html>") !== false); + $this->assertTrue(substr($html,"</html>") !== false); + $this->assertTrue(substr($html,"<body onload") !== false); + $this->assertTrue(substr($html,"</body>") !== false); + $this->assertTrue(substr($html,"<form") !== false); + $this->assertTrue(substr($html,"</form>") !== false); + } +} + +class Tests_Auth_OpenID_SuccessResponse extends PHPUnit_TestCase { + function setUp() + { + $this->endpoint = new Auth_OpenID_ServiceEndpoint(); + $this->endpoint->claimed_id = 'identity_url'; + } + + function test_extensionResponse() + { + $uri = "http://bogus.unittest/1.0"; + + $query = array( + 'openid.ns.unittest' => $uri, + 'openid.unittest.one' => '1', + 'openid.unittest.two' =>'2', + 'openid.sreg.nickname' => 'j3h', + 'openid.return_to' => 'return_to'); + + $message = Auth_OpenID_Message::fromPostArgs($query); + $resp = new Auth_OpenID_SuccessResponse($this->endpoint, $message); + + $utargs = $resp->extensionResponse($uri, false); + $this->assertEquals($utargs, array('one' => '1', 'two' => '2')); + $sregargs = $resp->extensionResponse(Auth_OpenID_SREG_NS_URI, false); + $this->assertEquals($sregargs, array('nickname' => 'j3h')); + } + + function test_extensionResponseSigned() + { + $args = array( + 'ns.sreg' => 'urn:sreg', + 'ns.unittest' => 'urn:unittest', + 'unittest.one' => '1', + 'unittest.two' => '2', + 'sreg.nickname' => 'j3h', + 'sreg.dob' => 'yesterday', + 'return_to' => 'return_to', + 'signed' => 'sreg.nickname,unittest.one,sreg.dob'); + + $signed_list = array('openid.sreg.nickname', + 'openid.unittest.one', + 'openid.sreg.dob', + 'openid.ns.sreg'); + + $msg = Auth_OpenID_Message::fromOpenIDArgs($args); + $resp = new Auth_OpenID_SuccessResponse($this->endpoint, $msg, $signed_list); + + // All args in this NS are signed, so expect all. + $sregargs = $resp->extensionResponse('urn:sreg', true); + $this->assertEquals($sregargs, + array('nickname' => 'j3h', + 'dob' => 'yesterday')); + + // Not all args in this NS are signed, so expect null when + // asking for them. + $utargs = $resp->extensionResponse('urn:unittest', true); + $this->assertEquals($utargs, null); + } + + function test_noReturnTo() + { + $message = Auth_OpenID_Message::fromPostArgs(array()); + $resp = new Auth_OpenID_SuccessResponse($this->endpoint, $message); + $this->assertTrue($resp->getReturnTo() === null); + } + + function test_returnTo() + { + $query = array('openid.return_to' => 'return_to'); + $message = Auth_OpenID_Message::fromPostArgs($query); + + $resp = new Auth_OpenID_SuccessResponse($this->endpoint, + $message, array('openid.return_to')); + + $this->assertEquals($resp->getReturnTo(), 'return_to'); + } +} + +class _StubConsumer { + function _StubConsumer() + { + $this->assoc = null; + $this->response = null; + $this->endpoint = null; + $this->fetcher = new _MockFetcher(); + } + + function begin($service) + { + $auth_req = new Auth_OpenID_AuthRequest($service, $this->assoc); + $this->endpoint = $service; + return $auth_req; + } + + function complete($message, $endpoint, $return_to) + { + return $this->response; + } +} + +class Tests_Auth_OpenID_DiscoFailure extends PHPUnit_TestCase { + var $consumerClass = null; + + function setUp() + { + foreach ($_SESSION as $k => $v) { + unset($_SESSION[$k]); + } + + $this->endpoint = new Auth_OpenID_ServiceEndpoint(); + $this->claimed_id = 'http://identity.url/'; + $this->endpoint->claimed_id = $this->claimed_id; + $this->store = null; + $this->session = new Auth_Yadis_PHPSession(); + $cls = $this->consumerClass; + $this->consumer =& new $cls($this->store, &$this->session); + $this->consumer->consumer =& new _StubConsumer(); + $this->discovery =& new Auth_Yadis_Discovery(&$this->session, + $this->claimed_id, + $this->consumer->session_key_prefix); + } +} + +class Consumer_completeEmptySession extends Auth_OpenID_GenericConsumer { + var $test_case = null; + var $text = "failed complete"; + + function complete($message, $endpoint, $return_to) + { + $this->test_case->assertTrue($endpoint === null); + return new Auth_OpenID_FailureResponse($endpoint, $this->text); + } +} + +class Tests_Auth_OpenID_ConsumerTest2 extends PHPUnit_TestCase { + function setUp() + { + foreach ($_SESSION as $k => $v) { + unset($_SESSION[$k]); + } + + $this->endpoint = new Auth_OpenID_ServiceEndpoint(); + $this->claimed_id = 'http://identity.url/'; + $this->endpoint->claimed_id = $this->claimed_id; + $this->store = null; + $this->session = new Auth_Yadis_PHPSession(); + $this->consumer =& new Auth_OpenID_Consumer($this->store, $this->session); + $this->consumer->consumer =& new _StubConsumer(); + $this->discovery =& new Auth_Yadis_Discovery(&$this->session, + $this->claimed_id, + $this->consumer->session_key_prefix); + } + + function test_beginWithoutDiscovery() + { + // Does this really test anything non-trivial? + $result = $this->consumer->beginWithoutDiscovery($this->endpoint); + + // The result is an auth request + $this->assertTrue(strtolower(get_class($result)) == + 'auth_openid_authrequest'); + + $loader = new Auth_OpenID_ServiceEndpointLoader(); + + // Side-effect of calling beginWithoutDiscovery is setting the + // session value to the endpoint attribute of the result + $this->assertTrue( + $loader->fromSession( + $this->session->get($this->consumer->_token_key)) == + $result->endpoint); + + // The endpoint that we passed in is the endpoint on the + // auth_request + $this->assertTrue($result->endpoint == $this->endpoint); + } + + function test_completeEmptySession() + { + $this->consumer->consumer = new Consumer_completeEmptySession($this->store); + $this->consumer->consumer->test_case =& $this; + + $response = $this->consumer->complete(null); + $this->assertTrue(Auth_OpenID::isFailure($response)); + $this->assertEquals($this->consumer->consumer->text, $response->message); + $this->assertTrue($response->identity_url === null); + } + + function _doResp($auth_req, $exp_resp) + { + // complete a transaction, using the expected response from + // the generic consumer. + $this->consumer->consumer->response = $exp_resp; + + // endpoint is stored in the session + // $this->assertTrue($this->session->data); + $this->assertTrue($_SESSION); + $resp = $this->consumer->complete(null); + + // All responses should have the same identity URL, and the + // session should be cleaned out + if ($this->endpoint->claimed_id != Auth_OpenID_IDENTIFIER_SELECT) { + $this->assertTrue($resp->identity_url == $this->claimed_id); + } + + $this->assertFalse(in_array($this->consumer->_token_key, + $_SESSION)); // this->session->data)); + + // Expected status response + $this->assertEquals($resp->status, $exp_resp->status); + + return $resp; + } + + function _doRespNoDisco($exp_resp) + { + // Set up a transaction without discovery + $auth_req = $this->consumer->beginWithoutDiscovery($this->endpoint); + $resp = $this->_doResp($auth_req, $exp_resp); + // There should be nothing left in the session once we have + // completed. + $this->assertFalse($this->session->contents()); + return $resp; + } + + /* + * Be sure that the session gets cleaned up when the response is + * successful and has a different URL than the one in the request. + */ + function test_successDifferentURL() + { + // Set up a request endpoint describing an IDP URL + $this->identity_url = 'http://idp.url/'; + $this->endpoint->claimed_id = $this->endpoint->local_id = Auth_OpenID_IDENTIFIER_SELECT; + + // Use a response endpoint with a different URL (asserted by + // the IDP) + $resp_endpoint = new Auth_OpenID_ServiceEndpoint(); + $resp_endpoint->claimed_id = "http://user.url/"; + + $resp = $this->_doRespDisco( + true, + mkSuccess($resp_endpoint, array())); + $this->assertTrue($this->discovery->getManager(true) === null); + } + + function test_noDiscoCompleteSuccessWithToken() + { + $message = Auth_OpenID_Message::fromPostArgs(array()); + $this->_doRespNoDisco(new Auth_OpenID_SuccessResponse($this->endpoint, + $message)); + } + + function test_noDiscoCompleteCancelWithToken() + { + $this->_doRespNoDisco(new Auth_OpenID_CancelResponse($this->endpoint)); + } + + function test_noDiscoCompleteFailure() + { + $msg = 'failed!'; + $resp = $this->_doRespNoDisco(new Auth_OpenID_FailureResponse($this->endpoint, $msg)); + $this->assertTrue($resp->message == $msg); + } + + function test_noDiscoCompleteSetupNeeded() + { + $setup_url = 'http://setup.url/'; + $resp = $this->_doRespNoDisco( + new Auth_OpenID_SetupNeededResponse($this->endpoint, $setup_url)); + $this->assertTrue($resp->setup_url == $setup_url); + } + + // To test that discovery is cleaned up, we need to initialize a + // Yadis manager, and have it put its values in the session. + function _doRespDisco($is_clean, $exp_resp) + { + // Set up and execute a transaction, with discovery + $this->discovery->createManager(array($this->endpoint), + $this->claimed_id); + $auth_req = $this->consumer->begin($this->claimed_id); + $resp = $this->_doResp($auth_req, $exp_resp); + + $manager = $this->discovery->getManager(); + if ($is_clean) { + $this->assertTrue($this->discovery->getManager() === null); + } else { + $this->assertFalse($this->discovery->getManager() === null); + } + + return $resp; + } + + // Cancel and success DO clean up the discovery process + function test_completeSuccess() + { + $message = Auth_OpenID_Message::fromPostArgs(array()); + $this->_doRespDisco(true, + new Auth_OpenID_SuccessResponse($this->endpoint, + $message)); + } + + function test_completeCancel() + { + $this->_doRespDisco(true, + new Auth_OpenID_CancelResponse($this->endpoint)); + } + + // Failure and setup_needed don't clean up the discovery process + function test_completeFailure() + { + $msg = 'failed!'; + $resp = $this->_doRespDisco(false, + new Auth_OpenID_FailureResponse($this->endpoint, $msg)); + $this->assertTrue($resp->message == $msg); + } + + function test_completeSetupNeeded() + { + $setup_url = 'http://setup.url/'; + $resp = $this->_doRespDisco(false, + new Auth_OpenID_SetupNeededResponse($this->endpoint, $setup_url)); + $this->assertTrue($resp->status == Auth_OpenID_SETUP_NEEDED); + $this->assertTrue($resp->setup_url == $setup_url); + } + + function test_begin() + { + $this->discovery->createManager(array($this->endpoint), + $this->claimed_id); + // Should not raise an exception + $auth_req = $this->consumer->begin($this->claimed_id); + $this->assertTrue(strtolower(get_class($auth_req)) === 'auth_openid_authrequest'); + $this->assertTrue($auth_req->endpoint == $this->endpoint); + $this->assertTrue($auth_req->endpoint == $this->consumer->consumer->endpoint); + $this->assertTrue($auth_req->assoc == $this->consumer->consumer->assoc); + } +} + +class IDPDrivenTest_Consumer1 extends ConfigurableConsumer { + var $iverified = array(); + var $endpoint = null; + var $failure_cb = null; + var $check_endpoint = null; + + function _idResCheckNonce($message, $endpoint) + { + return true; + } + + function _verifyDiscoveryResults($identifier, $endpoint) + { + call_user_func($this->failure_cb, + $endpoint === $this->check_endpoint); + $this->iverified[] = $this->endpoint; + return $this->endpoint; + } +} + +class IDPDrivenTest_Consumer2 extends ConfigurableConsumer { + function verifyDiscoveryResults($identifier, $endp) + { + return new Auth_OpenID_FailureResponse(null, + "Bogus"); + } +} + +class IDPDrivenTest extends PHPUnit_TestCase { + function setUp() + { + $this->store = new GoodAssocStore(); + $this->consumer = new ConfigurableConsumer($this->store); + $this->endpoint = new Auth_OpenID_ServiceEndpoint(); + $this->endpoint->server_url = "http://idp.unittest/"; + } + + function test_idpDrivenBegin() + { + // Testing here that the token-handling doesn't explode... + $this->assertTrue($this->consumer->begin($this->endpoint) !== null); + } + + function test_idpDrivenComplete() + { + global $GOODSIG; + + $this->consumer = new IDPDrivenTest_Consumer1($this->store); + $this->consumer->failure_cb = array(&$this, "assertTrue"); + $this->consumer->check_endpoint =& $this->endpoint; + + $identifier = '=directed_identifier'; + $message = Auth_OpenID_Message::fromPostArgs(array( + 'openid.identity'=> '=directed_identifier', + 'openid.return_to'=> 'x', + 'openid.assoc_handle'=> 'z', + 'openid.signed'=> 'identity,return_to', + 'openid.sig'=> $GOODSIG)); + + $endpoint = new Auth_OpenID_ServiceEndpoint(); + $endpoint->claimed_id = $identifier; + $endpoint->server_url = $this->endpoint->server_url; + $endpoint->local_id = $identifier; + + $this->consumer->disableReturnToChecking(); + + $this->consumer->endpoint =& $endpoint; + $response = $this->consumer->_doIdRes($message, $this->endpoint, null); + + $this->failUnlessSuccess($response); + + $this->assertEquals($response->identity_url, "=directed_identifier"); + + // assert that discovery attempt happens and returns good + $this->assertEquals($this->consumer->iverified, array($endpoint)); + } + + function test_idpDrivenCompleteFraud() + { + global $GOODSIG; + + $this->consumer = new IDPDrivenTest_Consumer2($this->store); + + // crap with an identifier that doesn't match discovery info + $message = Auth_OpenID_Message::fromPostArgs(array( + 'openid.identity'=> '=directed_identifier', + 'openid.return_to'=> 'x', + 'openid.assoc_handle'=> 'z', + 'openid.signed'=> 'identity,return_to', + 'openid.sig'=> $GOODSIG)); + + $this->consumer->disableReturnToChecking(); + + $result = $this->consumer->_doIdRes($message, $this->endpoint, null); + $this->assertTrue(Auth_OpenID::isFailure($result)); + } + + function failUnlessSuccess($response) + { + if ($response->status != Auth_OpenID_SUCCESS) { + $this->fail("Non-successful response (status is ".$response->status.")"); + } + } +} + +global $__test_otherServer_text; +$__test_otherServer_text = "__test_otherServer"; +class TestDiscoveryVerification_test_otherServer extends Auth_OpenID_GenericConsumer { + function _discoverAndVerify($to_match) + { + global $__test_otherServer_text; + return new Auth_OpenID_FailureResponse(null, $__test_otherServer_text); + } +} + +class TestDiscoveryVerification extends PHPUnit_TestCase { + var $services = array(); + + function discoveryFunc($identifier) + { + return array($identifier, $this->services); + } + + function setUp() + { + $this->store = new GoodAssocStore(); + $this->consumer = new Auth_OpenID_GenericConsumer($this->store); + $this->consumer->discoverMethod = array($this, + 'discoveryFunc'); + + $this->identifier = "http://idp.unittest/1337"; + $this->server_url = "http://endpoint.unittest/"; + + $this->message = Auth_OpenID_Message::fromPostArgs(array( + 'openid.ns'=> Auth_OpenID_OPENID2_NS, + 'openid.identity'=> $this->identifier, + 'openid.claimed_id'=> $this->identifier, + 'openid.op_endpoint'=> $this->server_url)); + + $this->endpoint = new Auth_OpenID_ServiceEndpoint(); + $this->endpoint->server_url = $this->server_url; + } + + function test_theGoodStuff() + { + $endpoint = new Auth_OpenID_ServiceEndpoint(); + $endpoint->type_uris = array(Auth_OpenID_TYPE_2_0); + $endpoint->claimed_id = $this->identifier; + $endpoint->server_url = $this->server_url; + $endpoint->local_id = $this->identifier; + $this->services = array($endpoint); + $r = $this->consumer->_verifyDiscoveryResults($this->message, $endpoint); + + $this->assertEquals($r, $endpoint); + } + + function test_otherServer() + { + global $__test_otherServer_text; + + // setup + $this->consumer = new TestDiscoveryVerification_test_otherServer($this->store); + $this->consumer->discoverMethod = array($this, + 'discoveryFunc'); + + // a set of things without the stuff + $endpoint = new Auth_OpenID_ServiceEndpoint(); + $endpoint->type_uris = array(Auth_OpenID_TYPE_2_0); + $endpoint->claimed_id = $this->identifier; + $endpoint->server_url = "http://the-MOON.unittest/"; + $endpoint->local_id = $this->identifier; + $this->services = array($endpoint); + + $result = $this->consumer->_verifyDiscoveryResults( + $this->message, $endpoint); + + $this->assertTrue(Auth_OpenID::isFailure($result)); + $this->assertTrue(strpos($result->message, $__test_otherServer_text) !== false); + } + + function test_foreignDelegate() + { + global $__test_otherServer_text; + + // setup + $this->consumer = new TestDiscoveryVerification_test_otherServer($this->store); + $this->consumer->discoverMethod = array($this, + 'discoveryFunc'); + + // a set of things with the server stuff but other delegate + $endpoint = new Auth_OpenID_ServiceEndpoint(); + $endpoint->type_uris = array(Auth_OpenID_TYPE_2_0); + $endpoint->claimed_id = $this->identifier; + $endpoint->server_url = $this->server_url; + $endpoint->local_id = "http://unittest/juan-carlos"; + + $result = $this->consumer->_verifyDiscoveryResults( + $this->message, $endpoint); + $this->assertTrue(Auth_OpenID::isFailure($result)); + $this->assertTrue(strpos($result->message, $__test_otherServer_text) !== false); + } + + function test_nothingDiscovered() + { + // a set of no things. + $this->services = array(); + $result = $this->consumer->_verifyDiscoveryResults( + $this->message, $this->endpoint); + $this->assertTrue(Auth_OpenID::isFailure($result)); + } +} + +class DummyEndpoint { + var $use_compatibility = false; + + function compatibilityMode() + { + return $this->use_compatibility; + } +} + +class TestCreateAssociationRequest extends PHPUnit_TestCase { + function setUp() + { + $this->endpoint = new DummyEndpoint(); + $s = null; + $this->consumer = new Auth_OpenID_GenericConsumer($s); + $this->assoc_type = 'HMAC-SHA1'; + } + + function test_noEncryptionSendsType() + { + $session_type = 'no-encryption'; + list($session, $args) = $this->consumer->_createAssociateRequest( + $this->endpoint, $this->assoc_type, $session_type); + + $this->assertTrue(is_a($session, 'Auth_OpenID_PlainTextConsumerSession')); + + $expected = Auth_OpenID_Message::fromOpenIDArgs( + array('ns' => Auth_OpenID_OPENID2_NS, + 'session_type'=>$session_type, + 'mode'=>'associate', + 'assoc_type'=>$this->assoc_type)); + + $this->assertEquals($expected->toPostArgs(), + $args->toPostArgs()); + } + + function test_noEncryptionSendsTypeHMACSHA256() + { + if (!Auth_OpenID_HMACSHA256_SUPPORTED) { + $this->pass(); + return; + } + + $session_type = 'no-encryption'; + $this->assoc_type = 'HMAC-SHA256'; + + list($session, $args) = $this->consumer->_createAssociateRequest( + $this->endpoint, $this->assoc_type, $session_type); + + $this->assertTrue(is_a($session, 'Auth_OpenID_PlainTextConsumerSession')); + + $expected = Auth_OpenID_Message::fromOpenIDArgs( + array('ns' => Auth_OpenID_OPENID2_NS, + 'session_type'=>$session_type, + 'mode'=>'associate', + 'assoc_type'=>$this->assoc_type)); + + $this->assertEquals($expected->toPostArgs(), + $args->toPostArgs()); + + $response = Auth_OpenID_Message::fromOpenIDArgs( + array('ns' => Auth_OpenID_OPENID2_NS, + 'session_type'=>$session_type, + 'assoc_type'=>$this->assoc_type, + 'expires_in' => '10000000000', + 'mac_key' => 'ZM9v', + 'assoc_handle' => 'turnme' + ) + ); + + $assoc = $this->consumer->_extractAssociation($response, $session); + + $this->assertTrue($assoc !== null); + $this->assertTrue(is_a($assoc, 'Auth_OpenID_Association')); + $this->assertTrue($assoc->assoc_type = $this->assoc_type); + $this->assertTrue($assoc->session_type = $session_type); + } + + function test_noEncryptionCompatibility() + { + $this->endpoint->use_compatibility = true; + $session_type = 'no-encryption'; + list($session, $args) = $this->consumer->_createAssociateRequest( + $this->endpoint, $this->assoc_type, $session_type); + + $this->assertTrue(is_a($session, 'Auth_OpenID_PlainTextConsumerSession')); + $this->assertEquals(Auth_OpenID_Message::fromOpenIDArgs(array('mode'=>'associate', + 'assoc_type'=>$this->assoc_type)), + $args); + } + + function test_dhSHA1Compatibility() + { + // Set the consumer's session type to a fast session since we + // need it here. + setConsumerSession($this->consumer); + + $this->endpoint->use_compatibility = true; + $session_type = 'DH-SHA1'; + list($session, $args) = $this->consumer->_createAssociateRequest( + $this->endpoint, $this->assoc_type, $session_type); + + $this->assertTrue(is_a($session, + 'Auth_OpenID_DiffieHellmanSHA1ConsumerSession')); + + // This is a random base-64 value, so just check that it's + // present. + $this->assertTrue($args->hasKey(Auth_OpenID_OPENID1_NS, 'dh_consumer_public')); + $args->delArg(Auth_OpenID_OPENID1_NS, 'dh_consumer_public'); + + // OK, session_type is set here and not for no-encryption + // compatibility + $expected = Auth_OpenID_Message::fromOpenIDArgs(array('mode'=>'associate', + 'session_type'=>'DH-SHA1', + 'assoc_type'=>$this->assoc_type, + 'dh_modulus'=> 'BfvStQ==', + 'dh_gen'=> 'Ag==')); + + $this->assertEquals($expected->toPostArgs(), + $args->toPostArgs()); + } +} + +class TestDiffieHellmanResponseParameters extends PHPUnit_TestCase { + var $session_cls = null; + var $message_namespace = null; + + function setUp() + { + // Pre-compute DH with small prime so tests run quickly. + $this->server_dh = new Auth_OpenID_DiffieHellman(100389557, 2); + $this->consumer_dh = new Auth_OpenID_DiffieHellman(100389557, 2); + + $lib =& Auth_OpenID_getMathLib(); + + $cls = $this->session_cls; + $this->consumer_session = new $cls($this->consumer_dh); + + // base64(btwoc(g ^ xb mod p)) + $this->dh_server_public = $lib->longToBase64($this->server_dh->public); + + $this->secret = Auth_OpenID_CryptUtil::randomString( + $this->consumer_session->secret_size); + + $this->enc_mac_key = base64_encode( + $this->server_dh->xorSecret($this->consumer_dh->public, + $this->secret, + $this->consumer_session->hash_func)); + + $this->msg = new Auth_OpenID_Message($this->message_namespace); + } + + function testExtractSecret() + { + $this->msg->setArg(Auth_OpenID_OPENID_NS, 'dh_server_public', + $this->dh_server_public); + + $this->msg->setArg(Auth_OpenID_OPENID_NS, 'enc_mac_key', + $this->enc_mac_key); + + $extracted = $this->consumer_session->extractSecret($this->msg); + $this->assertEquals($extracted, $this->secret); + } + + function testAbsentServerPublic() + { + $this->msg->setArg(Auth_OpenID_OPENID_NS, 'enc_mac_key', + $this->enc_mac_key); + + $this->assertTrue($this->consumer_session->extractSecret($this->msg) === null); + } + + function testAbsentMacKey() + { + $this->msg->setArg(Auth_OpenID_OPENID_NS, 'dh_server_public', + $this->dh_server_public); + + $this->assertTrue($this->consumer_session->extractSecret($this->msg) === null); + } + + /* + function testInvalidBase64Public() + { + $this->msg->setArg(Auth_OpenID_OPENID_NS, 'dh_server_public', + 'n o t b a s e 6 4.'); + + $this->msg->setArg(Auth_OpenID_OPENID_NS, 'enc_mac_key', + $this->enc_mac_key); + + $this->assertTrue($this->consumer_session->extractSecret($this->msg) === null); + } + + function testInvalidBase64MacKey() + { + $this->msg->setArg(Auth_OpenID_OPENID_NS, 'dh_server_public', + $this->dh_server_public); + + $this->msg->setArg(Auth_OpenID_OPENID_NS, 'enc_mac_key', + 'n o t base 64'); + + $this->assertTrue($this->consumer_session->extractSecret($this->msg) === null); + } + */ +} + +class TestOpenID1SHA1 extends TestDiffieHellmanResponseParameters { + var $session_cls = 'Auth_OpenID_DiffieHellmanSHA1ConsumerSession'; + var $message_namespace = Auth_OpenID_OPENID1_NS; +} + +class TestOpenID2SHA1 extends TestDiffieHellmanResponseParameters { + var $session_cls = 'Auth_OpenID_DiffieHellmanSHA1ConsumerSession'; + var $message_namespace = Auth_OpenID_OPENID2_NS; +} + +if (!defined('Auth_OpenID_NO_MATH_SUPPORT') && + Auth_OpenID_SHA256_SUPPORTED) { + class TestOpenID2SHA256 extends TestDiffieHellmanResponseParameters { + var $session_cls = 'Auth_OpenID_DiffieHellmanSHA256ConsumerSession'; + var $message_namespace = Auth_OpenID_OPENID2_NS; + } +} + +class Tests_Auth_OpenID_KVPost extends PHPUnit_TestCase { + function setUp() + { + $this->server_url = 'http://unittest/bogus'; + } + + function test_200() + { + $response = new Auth_Yadis_HTTPResponse(); + $response->status = 200; + $response->body = "foo:bar\nbaz:quux\n"; + $r = Auth_OpenID_GenericConsumer::_httpResponseToMessage($response, $this->server_url); + $expected_msg = Auth_OpenID_Message::fromOpenIDArgs(array('foo' => 'bar', 'baz' => 'quux')); + $this->assertEquals($expected_msg, $r); + } + + function test_400() + { + $response = new Auth_Yadis_HTTPResponse(); + $response->status = 400; + $response->body = "error:bonk\nerror_code:7\n"; + $result = Auth_OpenID_GenericConsumer::_httpResponseToMessage($response, $this->server_url); + + $this->assertTrue(is_a($result, 'Auth_OpenID_ServerErrorContainer')); + $this->assertEquals($result->error_text, 'bonk'); + $this->assertEquals($result->error_code, '7'); + } + + function test_500() + { + // 500 as an example of any non-200, non-400 code. + $response = new Auth_Yadis_HTTPResponse(); + $response->status = 500; + $response->body = "foo:bar\nbaz:quux\n"; + $result = Auth_OpenID_GenericConsumer::_httpResponseToMessage($response, $this->server_url); + $this->assertTrue($result === null); + } +} + +// Add other test cases to be run. +global $Tests_Auth_OpenID_Consumer_other; +$Tests_Auth_OpenID_Consumer_other = array( + // new Tests_Auth_OpenID_Consumer_TestSetupNeeded(), + new Tests_Auth_OpenID_AuthRequestHTMLMarkup(), + new Tests_Auth_OpenID_Consumer_TestCheckAuth(), + new Tests_Auth_OpenID_Consumer_TestCheckAuthTriggered(), + new Tests_Auth_OpenID_Consumer_TestFetchAssoc(), + new Tests_Auth_OpenID_Consumer_CheckNonceTest(), + new Tests_Auth_OpenID_Complete(), + new Tests_Auth_OpenID_SuccessResponse(), + new Tests_Auth_OpenID_CheckAuthResponse(), + new Tests_Auth_OpenID_FetchErrorInIdRes(), + new Tests_Auth_OpenID_ConsumerTest2(), + new Tests_Auth_OpenID_Stateless1(), + new Tests_Auth_OpenID_Stateless2(), + new TestCompleteMissingSig(), + new TestReturnToArgs(), + new IDPDrivenTest(), + new TestDiscoveryVerification(), + new Tests_Auth_OpenID_KVPost(), + new Tests_idResURLMismatch(), + new IdResCheckForFieldsTest(), + ); + +if (!defined('Auth_OpenID_NO_MATH_SUPPORT')) { + $Tests_Auth_OpenID_Consumer_other[] = new TestCreateAssociationRequest(); + $Tests_Auth_OpenID_Consumer_other[] = new TestOpenID1SHA1(); + $Tests_Auth_OpenID_Consumer_other[] = new TestOpenID2SHA1(); +} + +if (!defined('Auth_OpenID_NO_MATH_SUPPORT') && + Auth_OpenID_SHA256_SUPPORTED) { + $Tests_Auth_OpenID_Consumer_other[] = new TestOpenID2SHA256(); +} + +?> diff --git a/Tests/Auth/OpenID/CryptUtil.php b/Tests/Auth/OpenID/CryptUtil.php new file mode 100644 index 0000000..f158c1c --- /dev/null +++ b/Tests/Auth/OpenID/CryptUtil.php @@ -0,0 +1,56 @@ +<?php + +/** + * Tests for the CryptUtil functions. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'PHPUnit.php'; +require_once 'Auth/OpenID.php'; +require_once 'Auth/OpenID/CryptUtil.php'; + +class Tests_Auth_OpenID_CryptUtil extends PHPUnit_TestCase { + function test_length() + { + $cases = array(1, 10, 255); + foreach ($cases as $length) { + $data = Auth_OpenID_CryptUtil::getBytes($length); + $this->assertEquals(Auth_OpenID::bytes($data), $length); + } + } + + function test_different() + { + $num_iterations = 100; + $data_length = 20; + + $data = Auth_OpenID_CryptUtil::getBytes($num_iterations); + for ($i = 0; $i < $num_iterations; $i++) { + $last = $data; + $data = Auth_OpenID_CryptUtil::getBytes($data_length); + $this->assertFalse($data == $last); + } + } + + function test_cryptrand() + { + // It's possible, but HIGHLY unlikely that a correct + // implementation will fail by returning the same number twice + + $s = Auth_OpenID_CryptUtil::getBytes(32); + $t = Auth_OpenID_CryptUtil::getBytes(32); + $this->assertEquals(Auth_OpenID::bytes($s), 32); + $this->assertEquals(Auth_OpenID::bytes($t), 32); + $this->assertFalse($s == $t); + } +} + +?>
\ No newline at end of file diff --git a/Tests/Auth/OpenID/DiffieHellman.php b/Tests/Auth/OpenID/DiffieHellman.php new file mode 100644 index 0000000..080d909 --- /dev/null +++ b/Tests/Auth/OpenID/DiffieHellman.php @@ -0,0 +1,160 @@ +<?php + +/** + * Tests for the Diffie-Hellman key exchange implementation in the + * OpenID library. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'PHPUnit.php'; +require_once 'Auth/OpenID/DiffieHellman.php'; +require_once 'Tests/Auth/OpenID/TestUtil.php'; + +class Tests_Auth_OpenID_DiffieHellman_CheckCases extends PHPUnit_TestCase { + function Tests_Auth_OpenID_DiffieHellman_CheckCases($cases, $n) + { + $this->cases = $cases; + $this->n = $n; + } + + function runTest() + { + $this->assertEquals($this->n, count($this->cases)); + } +} + +class Tests_Auth_OpenID_DiffieHellman_Private extends PHPUnit_TestCase { + function Tests_Auth_OpenID_DiffieHellman_Private($name, $input, $expected) + { + $this->setName("$name"); + $this->input = $input; + $this->expected = $expected; + } + + function runTest() + { + $lib =& Auth_OpenID_getMathLib(); + $dh = new Auth_OpenID_DiffieHellman(null, null, $this->input); + $this->assertEquals($lib->cmp($this->expected, $dh->getPublicKey()), 0); + } +} + +class Tests_Auth_OpenID_DiffieHellman_Exch extends PHPUnit_TestCase { + function Tests_Auth_OpenID_DiffieHellman_Exch($name, $p1, $p2, $shared) + { + $this->setName("$name"); + $this->p1 = $p1; + $this->p2 = $p2; + $this->shared = $shared; + } + + function runTest() + { + $lib =& Auth_OpenID_getMathLib(); + $shared = $lib->init($this->shared); + $dh1 = new Auth_OpenID_DiffieHellman(null, null, $this->p1); + $dh2 = new Auth_OpenID_DiffieHellman(null, null, $this->p2); + $sh1 = $dh1->getSharedSecret($dh2->getPublicKey()); + $sh2 = $dh2->getSharedSecret($dh1->getPublicKey()); + $this->assertEquals($lib->cmp($shared, $sh1), 0); + $this->assertEquals($lib->cmp($shared, $sh2), 0); + } +} + +class Tests_Auth_OpenID_DiffieHellman extends PHPUnit_TestSuite { + function _readPrivateTestCases() + { + $lines = Tests_Auth_OpenID_readlines('dhpriv'); + $cases = array(); + foreach ($lines as $line) { + $case = array(); + if (!preg_match('/^(\d+) (\d+)\n$/', $line, $case)) { + trigger_error("Bad test input: $line", E_USER_ERROR); + } + + $c = count($case); + if ($c != 3) { + trigger_error("Wrong number of elements in parsed case: $c", + E_USER_ERROR); + } + + array_shift($case); + $cases[] = $case; + } + + return $cases; + } + + function _readExchTestCases() + { + $lines = Tests_Auth_OpenID_readlines('dhexch'); + $cases = array(); + foreach ($lines as $line) { + $case = array(); + if (!preg_match('/^(\d+) (\d+) (\d+)\n$/', $line, $case)) { + trigger_error("Bad test input: $line", E_USER_ERROR); + } + + $c = count($case); + if ($c != 4) { + trigger_error("Wrong number of elements in parsed case: $c", + E_USER_ERROR); + } + + array_shift($case); + $cases[] = $case; + } + return $cases; + } + + function Tests_Auth_OpenID_DiffieHellman($name) + { + $this->setName($name); + + $priv_cases = Tests_Auth_OpenID_DiffieHellman::_readPrivateTestCases(); + $sanity = new Tests_Auth_OpenID_DiffieHellman_CheckCases( + $priv_cases, 29); + $sanity->setName('Check parsing of priv test data'); + $this->addTest($sanity); + + $exch_cases = Tests_Auth_OpenID_DiffieHellman::_readExchTestCases(); + $sanity = new Tests_Auth_OpenID_DiffieHellman_CheckCases( + $exch_cases, 25); + $sanity->setName('Check parsing of exch test data'); + $this->addTest($sanity); + + if (!defined('Auth_OpenID_NO_MATH_SUPPORT')) { + if (defined('Tests_Auth_OpenID_thorough')) { + $npriv = count($priv_cases); + $nexch = count($exch_cases); + } else { + $npriv = 1; + $nexch = 3; + } + + for ($i = 0; $i < $npriv; $i++) { + list($input, $expected) = $priv_cases[$i]; + $one = new Tests_Auth_OpenID_DiffieHellman_Private( + "DHPriv $i", $input, $expected); + $this->addTest($one); + } + + for ($i = 0; $i < $nexch; $i++) { + $case = $exch_cases[$i]; + $one = new Tests_Auth_OpenID_DiffieHellman_Exch( + $i, $case[0], $case[1], $case[2]); + $this->addTest($one); + } + } + } +} + +?> diff --git a/Tests/Auth/OpenID/Discover_OpenID.php b/Tests/Auth/OpenID/Discover_OpenID.php new file mode 100644 index 0000000..a00873f --- /dev/null +++ b/Tests/Auth/OpenID/Discover_OpenID.php @@ -0,0 +1,783 @@ +<?php + +require_once 'PHPUnit.php'; +require_once 'TestUtil.php'; + +require_once 'Auth/OpenID.php'; +require_once 'Auth/OpenID/Discover.php'; +require_once 'Auth/Yadis/Manager.php'; +require_once 'Auth/Yadis/Misc.php'; +require_once 'Auth/Yadis/XRI.php'; + +/** + * Tests for the core of the PHP Yadis library discovery logic. + */ + +class _SimpleMockFetcher { + function _SimpleMockFetcher($responses) + { + $this->responses = $responses; + } + + function get($url) + { + $response = array_pop($this->responses); + assert($response[1] == $url); + return $response; + } +} + +class Tests_Auth_OpenID_ServiceEndpoint extends PHPUnit_TestCase { + function setUp() { + $this->endpoint = new Auth_OpenID_ServiceEndpoint(); + } + + function test_getDisplayIdentifier_noFragment() { + $urls = array("http://foo.bar.com/something", + "http://foo.bar.com/something?else=what¬hing=0", + "https://smoker.myopenid.com/" + ); + + foreach ($urls as $url) { + $this->endpoint->claimed_id = $url; + $this->assertEquals($url, $this->endpoint->getDisplayIdentifier()); + } + } + + function test_getDisplayIdentifier_withFragment() { + $urls = array("http://foo.bar.com/something#fragged", + "http://foo.bar.com/something?else=what¬hing=0#ow", + "https://smoker.myopenid.com/#myentirelife" + ); + + foreach ($urls as $url) { + $this->endpoint->claimed_id = $url; + $split = explode('#', $url); + $this->assertEquals($split[0], + $this->endpoint->getDisplayIdentifier()); + } + } +} + +class Tests_Auth_OpenID_DiscoveryFailure extends PHPUnit_TestCase { + + function Tests_Auth_OpenID_DiscoveryFailure($responses) + { + // Response is ($code, $url, $body). + $this->cases = array( + array(null, 'http://network.error/', ''), + array(404, 'http://not.found/', ''), + array(400, 'http://bad.request/', ''), + array(500, 'http://server.error/', ''), + array(200, 'http://header.found/', 200, + array('x-xrds-location' => 'http://xrds.missing/')), + array(404, 'http://xrds.missing/', '')); + + $this->url = $responses[0]->final_url; + $this->responses = $responses; + $this->fetcher = new _SimpleMockFetcher($this->responses); + } + + function runTest() + { + foreach ($this->cases as $case) { + list($status, $url, $body) = $case; + $expected_status = $status; + + $result = Auth_OpenID_discover($this->url, $this->fetcher); + list($id_url, $svclist) = $result; + + $this->assertEquals($svclist, array()); + } + } +} + +### Tests for raising/catching exceptions from the fetcher through the +### discover function + +class _ErrorRaisingFetcher { + // Just raise an exception when fetch is called + + function _ErrorRaisingFetcher($thing_to_raise) + { + $this->thing_to_raise = $thing_to_raise; + } + + function post($body = null) + { + __raiseError($this->thing_to_raise); + } + + function get($url) + { + __raiseError($this->thing_to_raise); + } +} + +define('E_AUTH_OPENID_EXCEPTION', 'e_exception'); +define('E_AUTH_OPENID_DIDFETCH', 'e_didfetch'); +define('E_AUTH_OPENID_VALUE_ERROR', 'e_valueerror'); +define('E_AUTH_OPENID_RUNTIME_ERROR', 'e_runtimeerror'); +define('E_AUTH_OPENID_OI', 'e_oi'); + +class Tests_Auth_OpenID_Discover_FetchException extends PHPUnit_TestCase { + // Make sure exceptions get passed through discover function from + // fetcher. + + function Tests_Auth_OpenID_Discover_FetchException($exc) + { + $this->cases = array(E_AUTH_OPENID_EXCEPTION, + E_AUTH_OPENID_DIDFETCH, + E_AUTH_OPENID_VALUE_ERROR, + E_AUTH_OPENID_RUNTIME_ERROR, + E_AUTH_OPENID_OI); + } + + function runTest() + { + foreach ($this->cases as $thing_to_raise) { + $fetcher = ErrorRaisingFetcher($thing_to_raise); + Auth_OpenID_discover('http://doesnt.matter/', $fetcher); + $exc = __getError(); + + if ($exc !== $thing_to_raise) { + $this->fail('FetchException expected %s to be raised', + $thing_to_raise); + } + } + } +} + + +// Tests for openid.consumer.discover.discover + +class _DiscoveryMockFetcher extends Auth_Yadis_HTTPFetcher { + function _DiscoveryMockFetcher(&$documents) + { + $this->redirect = null; + $this->documents = &$documents; + $this->fetchlog = array(); + } + + function supportsSSL() + { + return true; + } + + function post($url, $body = null, $headers = null) + { + return $this->get($url, $headers, $body); + } + + function get($url, $headers = null, $body = null) + { + $this->fetchlog[] = array($url, $body, $headers); + + if ($this->redirect) { + $final_url = $this->redirect; + } else { + $final_url = $url; + } + + if (array_key_exists($url, $this->documents)) { + list($ctype, $body) = $this->documents[$url]; + $status = 200; + } else { + $status = 404; + $ctype = 'text/plain'; + $body = ''; + } + + return new Auth_Yadis_HTTPResponse($final_url, $status, + array('content-type' => $ctype), $body); + } +} + +class _DiscoveryBase extends PHPUnit_TestCase { + var $id_url = "http://someuser.unittest/"; + var $fetcherClass = '_DiscoveryMockFetcher'; + + function _checkService($s, + $server_url, + $claimed_id=null, + $local_id=null, + $canonical_id=null, + $types=null, + $used_yadis=false, + $display_identifier=null) + { + $this->assertEquals($server_url, $s->server_url); + if ($types == array('2.0 OP')) { + $this->assertFalse($claimed_id); + $this->assertFalse($local_id); + $this->assertFalse($s->claimed_id); + $this->assertFalse($s->local_id); + $this->assertFalse($s->getLocalID()); + $this->assertFalse($s->compatibilityMode()); + $this->assertTrue($s->isOPIdentifier()); + $this->assertEquals($s->preferredNamespace(), + Auth_OpenID_OPENID2_NS); + } else { + $this->assertEquals($claimed_id, $s->claimed_id); + $this->assertEquals($local_id, $s->getLocalID()); + } + + if ($used_yadis) { + $this->assertTrue($s->used_yadis, "Expected to use Yadis"); + } else { + $this->assertFalse($s->used_yadis, + "Expected to use old-style discovery"); + } + + $openid_types = array( + '1.1' => Auth_OpenID_TYPE_1_1, + '1.0' => Auth_OpenID_TYPE_1_0, + '2.0' => Auth_OpenID_TYPE_2_0, + '2.0 OP' => Auth_OpenID_TYPE_2_0_IDP); + + $type_uris = array(); + foreach ($types as $t) { + $type_uris[] = $openid_types[$t]; + } + + $this->assertEquals($type_uris, $s->type_uris); + $this->assertEquals($canonical_id, $s->canonicalID); + + if ($s->canonicalID) { + $this->assertTrue($s->getDisplayIdentifier() != $claimed_id); + $this->assertTrue($s->getDisplayIdentifier() !== null); + $this->assertEquals($display_identifier, $s->getDisplayIdentifier()); + $this->assertEquals($s->claimed_id, $s->canonicalID); + } + + $this->assertEquals($s->display_identifier ? $s->display_identifier : $s->claimed_id, + $s->getDisplayIdentifier()); + } + + function setUp() + { + $cls = $this->fetcherClass; + // D is for Dumb. + $d = array(); + $this->fetcher = new $cls($d); + } +} + +class Tests_Auth_OpenID_Discover_OpenID extends _DiscoveryBase { + function _discover($content_type, $data, + $expected_services, $expected_id=null) + { + if ($expected_id === null) { + $expected_id = $this->id_url; + } + + $this->fetcher->documents[$this->id_url] = array($content_type, $data); + list($id_url, $services) = Auth_OpenID_discover($this->id_url, + $this->fetcher); + $this->assertEquals($expected_services, count($services)); + $this->assertEquals($expected_id, $id_url); + return $services; + } + + function test_404() + { + list($url, $services) = Auth_OpenID_discover($this->id_url . '/404', + $this->fetcher); + $this->assertTrue($services == array()); + } + + function test_noOpenID() + { + $services = $this->_discover('text/plain', + "junk", + 0); + + $services = $this->_discover( + 'text/html', + Tests_Auth_OpenID_readdata('test_discover_openid_no_delegate.html'), + 1); + + $this->_checkService($services[0], + "http://www.myopenid.com/server", + $this->id_url, + $this->id_url, + null, + array('1.1'), + false); + } + + function test_html1() + { + $services = $this->_discover('text/html', + Tests_Auth_OpenID_readdata('test_discover_openid.html'), + 1); + + + $this->_checkService($services[0], + "http://www.myopenid.com/server", + $this->id_url, + 'http://smoker.myopenid.com/', + null, + array('1.1'), + false, + $this->id_url); + } + + /* + * Ensure that the Claimed Identifier does not have a fragment if + * one is supplied in the User Input. + */ + function test_html1Fragment() + { + $data = Tests_Auth_OpenID_readdata('openid.html'); + $content_type = 'text/html'; + $expected_services = 1; + + $this->fetcher->documents[$this->id_url] = array($content_type, $data); + $expected_id = $this->id_url; + $this->id_url = $this->id_url . '#fragment'; + list($id_url, $services) = Auth_OpenID_discover($this->id_url, $this->fetcher); + $this->assertEquals($expected_services, count($services)); + $this->assertEquals($expected_id, $id_url); + + $this->_checkService( + $services[0], + "http://www.myopenid.com/server", + $expected_id, + 'http://smoker.myopenid.com/', + null, + array('1.1'), + false, + $this->id_url); + } + + function test_html2() + { + $services = $this->_discover('text/html', + Tests_Auth_OpenID_readdata('test_discover_openid2.html'), + 1); + + $this->_checkService($services[0], + "http://www.myopenid.com/server", + $this->id_url, + 'http://smoker.myopenid.com/', + null, + array('2.0'), + false, + $this->id_url); + } + + function test_html1And2() + { + $services = $this->_discover('text/html', + Tests_Auth_OpenID_readdata('test_discover_openid_1_and_2.html'), + 2); + + $types = array('2.0', '1.1'); + + for ($i = 0; $i < count($types); $i++) { + $t = $types[$i]; + $s = $services[$i]; + + $this->_checkService( + $s, + "http://www.myopenid.com/server", + $this->id_url, + 'http://smoker.myopenid.com/', + null, + array($t), + false, + $this->id_url); + } + } + + function test_yadisEmpty() + { + $services = $this->_discover('application/xrds+xml', + Tests_Auth_OpenID_readdata('test_discover_yadis_0entries.xml'), + 0); + } + + function test_htmlEmptyYadis() + { + // HTML document has discovery information, but points to an + // empty Yadis document. + + // The XRDS document pointed to by "openid_and_yadis.html" + $this->fetcher->documents[$this->id_url . 'xrds'] = + array('application/xrds+xml', + Tests_Auth_OpenID_readdata('test_discover_yadis_0entries.xml')); + + $services = $this->_discover('text/html', + Tests_Auth_OpenID_readdata('test_discover_openid_and_yadis.html'), + 1); + + $this->_checkService($services[0], + "http://www.myopenid.com/server", + $this->id_url, + 'http://smoker.myopenid.com/', + null, + array('1.1'), + false, + $this->id_url); + } + + function test_yadis1NoDelegate() + { + $services = $this->_discover('application/xrds+xml', + Tests_Auth_OpenID_readdata('test_discover_yadis_no_delegate.xml'), + 1); + + $this->_checkService( + $services[0], + "http://www.myopenid.com/server", + $this->id_url, + $this->id_url, + null, + array('1.0'), + true, + $this->id_url); + } + + function test_yadis2NoLocalID() + { + $services = $this->_discover('application/xrds+xml', + Tests_Auth_OpenID_readdata('test_discover_openid2_xrds_no_local_id.xml'), + 1); + + $this->_checkService( + $services[0], + "http://www.myopenid.com/server", + $this->id_url, + $this->id_url, + null, + array('2.0'), + true, + $this->id_url); + } + + function test_yadis2() + { + $services = $this->_discover('application/xrds+xml', + Tests_Auth_OpenID_readdata('test_discover_openid2_xrds.xml'), + 1); + + $this->_checkService($services[0], + "http://www.myopenid.com/server", + $this->id_url, + 'http://smoker.myopenid.com/', + null, + array('2.0'), + true, + $this->id_url); + } + + function test_yadis2OP() + { + $services = $this->_discover('application/xrds+xml', + Tests_Auth_OpenID_readdata('test_discover_yadis_idp.xml'), + 1); + + $this->_checkService($services[0], + "http://www.myopenid.com/server", + null, + null, + null, + array('2.0 OP'), + true, + $this->id_url); + } + + function test_yadis2OPDelegate() + { + // The delegate tag isn't meaningful for OP entries. + $services = $this->_discover('application/xrds+xml', + Tests_Auth_OpenID_readdata('test_discover_yadis_idp_delegate.xml'), + 1); + + $this->_checkService( + $services[0], + "http://www.myopenid.com/server", + null, null, null, + array('2.0 OP'), + true, + $this->id_url); + } + + function test_yadis2BadLocalID() + { + $services = $this->_discover('application/xrds+xml', + Tests_Auth_OpenID_readdata('test_discover_yadis_2_bad_local_id.xml'), + 0); + } + + function test_yadis1And2() + { + $services = $this->_discover('application/xrds+xml', + Tests_Auth_OpenID_readdata('test_discover_openid_1_and_2_xrds.xml'), + 1); + + $this->_checkService( + $services[0], + "http://www.myopenid.com/server", + $this->id_url, + 'http://smoker.myopenid.com/', + null, + array('2.0', '1.1'), + true); + } + + function test_yadis1And2BadLocalID() + { + $services = $this->_discover('application/xrds+xml', + Tests_Auth_OpenID_readdata('test_discover_openid_1_and_2_xrds_bad_delegate.xml'), + 0); + } +} + +class _MockFetcherForXRIProxy extends Auth_Yadis_HTTPFetcher { + + function _MockFetcherForXRIProxy($documents) + { + $this->documents = $documents; + $this->fetchlog = array(); + } + + function get($url, $headers=null) + { + return $this->fetch($url, $headers); + } + + function post($url, $body) + { + return $this->fetch($url, $body); + } + + function fetch($url, $body=null, $headers=null) + { + $this->fetchlog[] = array($url, $body, $headers); + + $u = parse_url($url); + $proxy_host = $u['host']; + $xri = $u['path']; + $query = Auth_OpenID::arrayGet($u, 'query'); + + if ((!$headers) && (!$query)) { + trigger_error('Error in mock XRI fetcher: no headers or query'); + } + + if (Auth_Yadis_startswith($xri, '/')) { + $xri = substr($xri, 1); + } + + if (array_key_exists($xri, $this->documents)) { + list($ctype, $body) = $this->documents[$xri]; + $status = 200; + } else { + $status = 404; + $ctype = 'text/plain'; + $body = ''; + } + + return new Auth_Yadis_HTTPResponse($url, $status, + array('content-type' => $ctype), + $body); + } +} + +class TestXRIDiscovery extends _DiscoveryBase { + var $fetcherClass = '_MockFetcherForXRIProxy'; + + function setUp() { + parent::setUp(); + + $this->fetcher->documents = array('=smoker' => array('application/xrds+xml', + Tests_Auth_OpenID_readdata('yadis_2entries_delegate.xml')), + '=smoker*bad' => array('application/xrds+xml', + Tests_Auth_OpenID_readdata('yadis_another_delegate.xml'))); + } + + function test_xri() { + list($user_xri, $services) = Auth_OpenID_discoverXRI('=smoker'); + + $this->_checkService( + $services[0], + "http://www.myopenid.com/server", + Auth_Yadis_XRI("=!1000"), + 'http://smoker.myopenid.com/', + Auth_Yadis_XRI("=!1000"), + array('1.0'), + true, + '=smoker'); + + $this->_checkService( + $services[1], + "http://www.livejournal.com/openid/server.bml", + Auth_Yadis_XRI("=!1000"), + 'http://frank.livejournal.com/', + Auth_Yadis_XRI("=!1000"), + array('1.0'), + true, + '=smoker'); + } + + function test_xriNoCanonicalID() { + list($user_xri, $services) = Auth_OpenID_discoverXRI('=smoker*bad'); + $this->assertFalse($services); + } + + function test_useCanonicalID() { + $endpoint = new Auth_OpenID_ServiceEndpoint(); + $endpoint->claimed_id = Auth_Yadis_XRI("=!1000"); + $endpoint->canonicalID = Auth_Yadis_XRI("=!1000"); + $htis->assertEquals($endpoint->getLocalID(), Auth_Yadis_XRI("=!1000")); + } +} + +class Tests_Auth_OpenID_DiscoverSession { + function Tests_Auth_OpenID_DiscoverSession() + { + $this->data = array(); + } + + function set($name, $value) + { + $this->data[$name] = $value; + } + + function get($name, $default=null) + { + if (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } else { + return $default; + } + } + + function del($name) + { + unset($this->data[$name]); + } +} + +global $__Tests_BOGUS_SERVICE; +$__Tests_BOGUS_SERVICE = new Auth_OpenID_ServiceEndpoint(); +$__Tests_BOGUS_SERVICE->claimed_id = "=really.bogus.endpoint"; + +function __serviceCheck_discover_cb($url, $fetcher) +{ + global $__Tests_BOGUS_SERVICE; + return array($url, array($__Tests_BOGUS_SERVICE)); +} + +class _FetcherWithSSL extends _DiscoveryMockFetcher { + function supportsSSL() + { + return true; + } +} + +class _FetcherWithoutSSL extends _DiscoveryMockFetcher { + function supportsSSL() + { + return false; + } +} + +class _NonFetcher extends _DiscoveryMockFetcher { + var $used = false; + + function _NonFetcher() + { + $a = array(); + parent::_DiscoveryMockFetcher($a); + } + + function supportsSSL() + { + return false; + } + + function get($url, $headers) + { + $this->used = true; + } +} + +class Tests_Auth_OpenID_SSLSupport extends PHPUnit_TestCase { + function test_discoverDropSSL() + { + // In the absence of SSL support, the discovery process should + // drop endpoints whose server URLs are HTTPS. + $id_url = 'http://bogus/'; + + $d = array( + $id_url => array('application/xrds+xml', + Tests_Auth_OpenID_readdata('test_discover_openid_ssl.xml')) + ); + + $f =& new _FetcherWithoutSSL($d); + + $result = Auth_OpenID_discover($id_url, $f); + + list($url, $services) = $result; + + $this->assertTrue($url == $id_url); + $this->assertTrue(count($services) == 1); + + $e = $services[0]; + $this->assertTrue($e->server_url == 'http://nossl.vroom.unittest/server'); + } + + function test_discoverRetainSSL() + { + // In the presence of SSL support, the discovery process + // should NOT drop endpoints whose server URLs are HTTPS. + + // In the absence of SSL support, the discovery process should + // drop endpoints whose server URLs are HTTPS. + $id_url = 'http://bogus/'; + + $d = array( + $id_url => array('application/xrds+xml', + Tests_Auth_OpenID_readdata('test_discover_openid_ssl.xml')) + ); + + $f =& new _FetcherWithSSL($d); + + $result = Auth_OpenID_discover($id_url, $f); + + list($url, $services) = $result; + + $this->assertTrue($url == $id_url); + $this->assertTrue(count($services) == 2); + + $e = $services[0]; + $this->assertTrue($e->server_url == 'http://nossl.vroom.unittest/server'); + + $e = $services[1]; + $this->assertTrue($e->server_url == 'https://ssl.vroom.unittest/server'); + } + + function test_discoverSSL() + { + // The consumer code should not attempt to perform discovery + // on an HTTPS identity URL in the absence of SSL support. + + $id_url = 'https://unsupported/'; + + $f =& new _NonFetcher(); + + $result = Auth_OpenID_discover($id_url, $f); + + $this->assertTrue($result == array($id_url, array())); + $this->assertFalse($f->used); + } +} + +global $Tests_Auth_OpenID_Discover_OpenID_other; +$Tests_Auth_OpenID_Discover_OpenID_other = array( + new Tests_Auth_OpenID_SSLSupport() + ); + +?>
\ No newline at end of file diff --git a/Tests/Auth/OpenID/Extension.php b/Tests/Auth/OpenID/Extension.php new file mode 100644 index 0000000..477110d --- /dev/null +++ b/Tests/Auth/OpenID/Extension.php @@ -0,0 +1,46 @@ +<?php + +require_once 'PHPUnit.php'; +require_once 'Auth/OpenID/Message.php'; +require_once 'Auth/OpenID/Extension.php'; + +class _ExtensionTest_DummyExtension extends Auth_OpenID_Extension { + var $ns_uri = 'http://an.extension/'; + var $ns_alias = 'dummy'; + + function getExtensionArgs() + { + return array(); + } +} + +class Tests_Auth_OpenID_Extension extends PHPUnit_TestCase { + function test_OpenID1() + { + $oid1_msg = new Auth_OpenID_Message(Auth_OpenID_OPENID1_NS); + $ext = new _ExtensionTest_DummyExtension(); + $ext->toMessage($oid1_msg); + $namespaces = $oid1_msg->namespaces; + + $this->assertTrue($namespaces->isImplicit($ext->ns_uri)); + $this->assertEquals($ext->ns_uri, + $namespaces->getNamespaceURI($ext->ns_alias)); + $this->assertEquals($ext->ns_alias, + $namespaces->getAlias($ext->ns_uri)); + } + + function test_OpenID2() + { + $oid2_msg = new Auth_OpenID_Message(Auth_OpenID_OPENID2_NS); + $ext = new _ExtensionTest_DummyExtension(); + $ext->toMessage($oid2_msg); + $namespaces = $oid2_msg->namespaces; + $this->assertFalse($namespaces->isImplicit($ext->ns_uri)); + $this->assertEquals($ext->ns_uri, + $namespaces->getNamespaceURI($ext->ns_alias)); + $this->assertEquals($ext->ns_alias, + $namespaces->getAlias($ext->ns_uri)); + } +} + +?>
\ No newline at end of file diff --git a/Tests/Auth/OpenID/HMAC.php b/Tests/Auth/OpenID/HMAC.php new file mode 100644 index 0000000..874892a --- /dev/null +++ b/Tests/Auth/OpenID/HMAC.php @@ -0,0 +1,166 @@ +<?php + +/** + * Tests for the HMAC-SHA1 utility functions used by the OpenID + * library. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'PHPUnit.php'; +require_once 'Auth/OpenID/HMAC.php'; +require_once 'Tests/Auth/OpenID/TestUtil.php'; + +class Tests_Auth_OpenID_HMAC_TestCase extends PHPUnit_TestCase { + function Tests_Auth_OpenID_HMAC_TestCase( + $name, $key, $data, $expected, $hmac_func) + { + + $this->setName($name); + $this->key = $key; + $this->data = $data; + $this->expected = $expected; + $this->hmac_func = $hmac_func; + } + + function runTest() + { + $actual = call_user_func($this->hmac_func, $this->key, $this->data); + $this->assertEquals(bin2hex($this->expected), bin2hex($actual)); + } +} + +class Tests_Auth_OpenID_HMAC extends PHPUnit_TestSuite { + function _strConvert($s) + { + $repeat_pat = '/^0x([a-f0-9]{2}) repeated (\d+) times$/'; + if (preg_match($repeat_pat, $s, $match)) { + $c = chr(hexdec($match[1])); + $n = $match[2]; + $data = ''; + for ($i = 0; $i < $n; $i++) { + $data .= $c; + } + } elseif (substr($s, 0, 2) == "0x") { + $data = pack('H*', substr($s, 2, strlen($s) - 1)); + } elseif (preg_match('/^"(.*)"$/', $s, $match)) { + $data = $match[1]; + } else { + trigger_error("Bad data format: $s", E_USER_ERROR); + } + return $data; + } + + function _readTestCases($test_file_name, $digest_len) + { + $lines = Tests_Auth_OpenID_readlines($test_file_name); + $cases = array(); + $case = array(); + foreach ($lines as $line) { + if ($line{0} == "#") { + continue; + } + + // Blank line separates test cases + if ($line == "\n") { + $cases[] = $case; + $case = array(); + } else { + $match = array(); + $pat = '/^([a-z0-9_-]+) =\s+(.*?)\n$/'; + if (!preg_match($pat, $line, $match)) { + trigger_error("Bad test input: $line", E_USER_ERROR); + } + + $c = count($match); + if ($c != 3) { + trigger_error( + "Wrong number of elements in parsed case: $c", + E_USER_ERROR); + return false; + } + + $key = $match[1]; + $value = $match[2]; + $case[$key] = $value; + } + } + + if (count($case)) { + $cases[] = $case; + } + + $final = array(); + + // Normalize strings and check data integrity + foreach ($cases as $case) { + $clean = array(); + $clean["key"] = + Tests_Auth_OpenID_HMAC::_strConvert($case["key"]); + if (defined(@$case["key_len"])) { + if (Auth_OpenID::bytes($clean["key"]) != $case["key_len"]) { + trigger_error("Bad key length", E_USER_ERROR); + } + } + + $clean["data"] = + Tests_Auth_OpenID_HMAC::_strConvert($case["data"]); + if (defined(@$case["data_len"])) { + if (Auth_OpenID::bytes($clean["data"]) != $case["data_len"]) { + trigger_error("Bad data length", E_USER_ERROR); + } + } + + $clean["digest"] = + Tests_Auth_OpenID_HMAC::_strConvert($case["digest"]); + if (Auth_OpenID::bytes($clean["digest"]) != $digest_len) { + $l = Auth_OpenID::bytes($clean["digest"]); + trigger_error("Bad digest length: $l", E_USER_ERROR); + } + + $clean['test_case'] = $case['test_case']; + + $final[] = $clean; + } + return $final; + } + + function Tests_Auth_OpenID_HMAC($name) + { + $this->setName($name); + $hash_test_defs = array(array( + 'Auth_OpenID_HMACSHA1', 'hmac-sha1.txt', 20)); + if (Auth_OpenID_HMACSHA256_SUPPORTED) { + $hash_test_defs[] = + array('Auth_OpenID_HMACSHA256', 'hmac-sha256.txt', 32); + } + foreach ($hash_test_defs as $params) { + list($hash_func, $filename, $hash_len) = $params; + $cases = $this->_readTestCases($filename, $hash_len); + foreach ($cases as $case) { + $test = new Tests_Auth_OpenID_HMAC_TestCase( + $case['test_case'], + $case['key'], + $case['data'], + $case['digest'], + $hash_func); + + $digest = $case['digest']; + $this->_addTestByValue($test); + } + } + } + + function _addTestByValue($test) { + $this->addTest($test); + } +} + +?> diff --git a/Tests/Auth/OpenID/KVForm.php b/Tests/Auth/OpenID/KVForm.php new file mode 100644 index 0000000..eb7162a --- /dev/null +++ b/Tests/Auth/OpenID/KVForm.php @@ -0,0 +1,261 @@ +<?php + +/** + * Tests for the KVForm module. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'PHPUnit.php'; +require_once 'Auth/OpenID/KVForm.php'; + +global $_Tests_Auth_OpenID_kverrors; +$_Tests_Auth_OpenID_kverrors = null; + +/** + * Keep a list of the logged errors + */ +function Tests_Auth_OpenID_kvHandleError($errno, $errmsg) +{ + global $_Tests_Auth_OpenID_kverrors; + $_Tests_Auth_OpenID_kverrors[] = $errmsg; +} + + +class Tests_Auth_OpenID_KVForm_TestCase extends PHPUnit_TestCase { + var $errs; + + function runTest() + { + // Re-set the number of logged errors + global $_Tests_Auth_OpenID_kverrors; + $_Tests_Auth_OpenID_kverrors = array(); + + set_error_handler("Tests_Auth_OpenID_kvHandleError"); + + $this->_runTest(); + + // Check to make sure we have the expected number of logged errors + //$this->assertEquals($this->errs, count($_Tests_Auth_OpenID_kverrors)); + + restore_error_handler(); + } + + function _runTest() + { + trigger_error('Must be overridden', E_USER_ERROR); + } +} + +class Tests_Auth_OpenID_KVForm_TestCase_Parse +extends Tests_Auth_OpenID_KVForm_TestCase { + function Tests_Auth_OpenID_KVForm_TestCase_Parse( + $arr, $str, $lossy, $errs) + { + + $this->arr = $arr; + $this->str = $str; + $this->lossy = $lossy; + $this->errs = $errs; + } + + function _runTest() + { + // Do one parse, after which arrayToKV and kvToArray should be + // inverses. + $parsed1 = Auth_OpenID_KVForm::toArray($this->str); + $serial1 = Auth_OpenID_KVForm::fromArray($this->arr); + + if ($this->lossy == "neither" || $this->lossy == "str") { + $this->assertEquals($this->arr, $parsed1, "str was lossy"); + } + + if ($this->lossy == "neither" || $this->lossy == "arr") { + $this->assertEquals($this->str, $serial1, "array was lossy"); + } + + $parsed2 = Auth_OpenID_KVForm::toArray($serial1); + $serial2 = Auth_OpenID_KVForm::fromArray($parsed1); + + // Round-trip both + $parsed3 = Auth_OpenID_KVForm::toArray($serial2); + $serial3 = Auth_OpenID_KVForm::fromArray($parsed2); + + $this->assertEquals($serial2, $serial3, "serialized forms differ"); + + // Check to make sure that they're inverses. + $this->assertEquals($parsed2, $parsed3, "parsed forms differ"); + + } +} + +class Tests_Auth_OpenID_KVForm_TestCase_Null +extends Tests_Auth_OpenID_KVForm_TestCase { + function Tests_Auth_OpenID_KVForm_TestCase_Null($arr, $errs) + { + $this->arr = $arr; + $this->errs = $errs; + } + + function _runTest() + { + $serialized = Auth_OpenID_KVForm::fromArray($this->arr); + $this->assertTrue($serialized === null, + 'serialization unexpectedly succeeded'); + } +} + +class Tests_Auth_OpenID_KVForm extends PHPUnit_TestSuite { + function Tests_Auth_OpenID_KVForm($name) + { + $this->setName($name); + $testdata_list = array( + array("name" => "simple", + "str" => "college:harvey mudd\n", + "arr" => array("college" => "harvey mudd"), + ), + array("name" => "empty", + "str" => "", + "arr" => array(), + ), + array("name" => "empty (just newline)", + "str" => "\n", + "arr" => array(), + "lossy" => "str", + "errors" => 1, + ), + array("name" => "empty (double newline)", + "str" => "\n\n", + "arr" => array(), + "lossy" => "str", + "errors" => 2, + ), + array("name" => "empty (no colon)", + "str" => "East is least\n", + "arr" => array(), + "lossy" => "str", + "errors" => 1, + ), + array("name" => "two keys", + "str" => "city:claremont\nstate:CA\n", + "arr" => array('city' => 'claremont', + 'state' => 'CA'), + ), + array("name" => "real life", + "str" => "is_valid:true\ninvalidate_handle:" . + "{HMAC-SHA1:2398410938412093}\n", + "arr" => array('is_valid' => 'true', + 'invalidate_handle' => + '{HMAC-SHA1:2398410938412093}'), + ), + array("name" => "empty key and value", + "str" => ":\n", + "arr" => array(''=>''), + ), + array("name" => "empty key, not value", + "str" => ":missing key\n", + "arr" => array(''=>'missing key'), + ), + array("name" => "whitespace at front of key", + "str" => " street:foothill blvd\n", + "arr" => array('street'=>'foothill blvd'), + "lossy" => "str", + "errors" => 1, + ), + array("name" => "whitespace at front of value", + "str" => "major: computer science\n", + "arr" => array('major'=>'computer science'), + "lossy" => "str", + "errors" => 1, + ), + array("name" => "whitespace around key and value", + "str" => " dorm : east \n", + "arr" => array('dorm'=>'east'), + "lossy" => "str", + "errors" => 2, + ), + array("name" => "missing trailing newline", + "str" => "e^(i*pi)+1:0", + "arr" => array('e^(i*pi)+1'=>'0'), + "lossy" => "str", + "errors" => 1, + ), + array("name" => "missing trailing newline (two key)", + "str" => "east:west\nnorth:south", + "arr" => array('east'=>'west', + 'north'=>'south'), + "lossy" => "str", + "errors" => 1, + ), + array("name" => "colon in key", + "arr" => array("k:k" => 'v'), + "errors" => 1, + ), + array("name" => "newline in key", + "arr" => array("k\nk" => 'v'), + "errors" => 1, + ), + array("name" => "newline in value", + "arr" => array('k' => "v\nv"), + "errors" => 1, + ), + array("name" => "array whitespace", + "arr" => array(" k " => "v"), + "lossy" => "both", + "str" => " k :v\n", + "errors" => 2, + ), + array("name" => "array ordering 1", + "arr" => array("a" => "x", + "b" => "x", + "c" => "x"), + "str" => "a:x\nb:x\nc:x\n", + ), + array("name" => "array ordering 2", + "arr" => array("a" => "x", + "c" => "x", + "b" => "x"), + "str" => "a:x\nc:x\nb:x\n", + ), + ); + + foreach ($testdata_list as $testdata) { + if (isset($testdata['str'])) { + $str = $testdata['str']; + } else { + $str = null; + } + + $arr = $testdata["arr"]; + + if (isset($testdata['errors'])) { + $errs = $testdata["errors"]; + } else { + $errs = 0; + } + + if (is_null($str)) { + $test = new Tests_Auth_OpenID_KVForm_TestCase_Null($arr, $errs); + } else { + if (isset($testdata['lossy'])) { + $lossy = $testdata["lossy"]; + } else { + $lossy = 'neither'; + } + $test = new Tests_Auth_OpenID_KVForm_TestCase( + $arr, $str, $lossy, $errs); + } + $test->setName($testdata["name"]); + $this->addTest($test); + } + } +} + +?> diff --git a/Tests/Auth/OpenID/MemStore.php b/Tests/Auth/OpenID/MemStore.php new file mode 100644 index 0000000..a45aae9 --- /dev/null +++ b/Tests/Auth/OpenID/MemStore.php @@ -0,0 +1,176 @@ +<?php + +/** + * In-memory OpenID store implementation for testing only + */ +require_once "Auth/OpenID/Interface.php"; +require_once 'Auth/OpenID/Nonce.php'; + +class ServerAssocs { + function ServerAssocs() + { + $this->assocs = array(); + } + + function set($assoc) + { + $this->assocs[$assoc->handle] = $assoc; + } + + function get($handle) + { + return Auth_OpenID::arrayGet($this->assocs, $handle); + } + + function remove($handle) + { + if (array_key_exists($handle, $this->assocs)) { + unset($this->assocs[$handle]); + return true; + } else { + return false; + } + } + + /* + * Returns association with the oldest issued date. + * + * or null if there are no associations. + */ + function best() + { + $best = null; + foreach ($this->assocs as $handle => $assoc) { + if (($best === null) || ($best->issued < $assoc->issued)) { + $best = $assoc; + } + } + return $best; + } + + /* + * Remove expired associations. + * + * @return (removed associations, remaining associations) + */ + function cleanup() + { + $remove = array(); + foreach ($this->assocs as $handle => $assoc) { + if ($assoc->getExpiresIn() == 0) { + $remove[] = $handle; + } + } + + foreach ($remove as $handle) { + unset($this->assocs[$handle]); + } + + return array(count($remove), count($this->assocs)); + } +} + +/* + * In-process memory store. + * + * Use for single long-running processes. No persistence supplied. + */ +class Tests_Auth_OpenID_MemStore extends Auth_OpenID_OpenIDStore { + function Tests_Auth_OpenID_MemStore() + { + $this->server_assocs = array(); + $this->nonces = array(); + } + + function &_getServerAssocs($server_url) + { + if (!array_key_exists($server_url, $this->server_assocs)) { + $this->server_assocs[$server_url] =& new ServerAssocs(); + } + + return $this->server_assocs[$server_url]; + } + + function storeAssociation($server_url, $assoc) + { + $assocs =& $this->_getServerAssocs($server_url); + $assocs->set($assoc); + } + + function getAssociation($server_url, $handle=null) + { + $assocs =& $this->_getServerAssocs($server_url); + if ($handle === null) { + return $assocs->best(); + } else { + return $assocs->get($handle); + } + } + + function removeAssociation($server_url, $handle) + { + $assocs =& $this->_getServerAssocs($server_url); + return $assocs->remove($handle); + } + + function useNonce($server_url, $timestamp, $salt) + { + global $Auth_OpenID_SKEW; + + if (abs($timestamp - time()) > $Auth_OpenID_SKEW) { + return false; + } + + $anonce = array($server_url, intval($timestamp), $salt); + + if (in_array($anonce, $this->nonces)) { + return false; + } else { + array_push($this->nonces, $anonce); + return true; + } + } + + function cleanupNonces() + { + global $Auth_OpenID_SKEW; + + $now = time(); + $expired = array(); + foreach ($this->nonces as $anonce) { + if (abs($anonce[1] - $now) > $Auth_OpenID_SKEW) { + // removing items while iterating over the set could + // be bad. + $expired[] = $anonce; + } + } + + foreach ($expired as $anonce) { + unset($this->nonces[array_search($anonce, $this->nonces)]); + } + + return count($expired); + } + + function cleanupAssociations() + { + $remove_urls = array(); + $removed_assocs = 0; + foreach ($this->server_assocs as $server_url => $assocs) { + list($removed, $remaining) = $assocs->cleanup(); + $removed_assocs += $removed; + if (!$remaining) { + $remove_urls[] = $server_url; + } + } + + // Remove entries from server_assocs that had none remaining. + foreach ($remove_urls as $server_url) { + unset($this->server_assocs[$server_url]); + } + + return $removed_assocs; + } +} + +?> diff --git a/Tests/Auth/OpenID/Message.php b/Tests/Auth/OpenID/Message.php new file mode 100644 index 0000000..7f86848 --- /dev/null +++ b/Tests/Auth/OpenID/Message.php @@ -0,0 +1,1252 @@ +<?php + +/** + * Unit tests for the Auth_OpenID_Message implementation. + */ + +require_once "Auth/OpenID/Message.php"; +require_once "Auth/OpenID.php"; +require_once "PHPUnit.php"; + +class MessageTest extends PHPUnit_TestCase { + function _argTest($ns, $key, $expected = null) + { + $a_default = 'a bogus default value'; + + $this->assertEquals($this->msg->getArg($ns, $key), $expected); + if ($expected === null) { + $this->assertEquals( + $this->msg->getArg($ns, $key, $a_default), $a_default); + $result = $this->msg->getArg($ns, $key, Auth_OpenID_NO_DEFAULT); + $this->assertTrue(Auth_OpenID::isFailure($result)); + } else { + $this->assertEquals( + $this->msg->getArg($ns, $key, $a_default), $expected); + $this->assertEquals( + $this->msg->getArg($ns, $key, Auth_OpenID_NO_DEFAULT), + $expected); + } + } +} + +class Tests_Auth_OpenID_EmptyMessage extends MessageTest { + function setUp() + { + $this->msg = new Auth_OpenID_Message(); + } + + function test_toPostArgs() + { + $this->assertEquals($this->msg->toPostArgs(), array()); + } + + function test_toArgs() + { + $this->assertEquals($this->msg->toArgs(), array()); + } + + function test_toKVForm() + { + $this->assertEquals($this->msg->toKVForm(), ''); + } + + function test_toURLEncoded() + { + $this->assertEquals($this->msg->toURLEncoded(), ''); + } + + function test_toURL() + { + $base_url = 'http://base.url/'; + $this->assertEquals($this->msg->toURL($base_url), $base_url); + } + + function test_getOpenID() + { + $this->assertEquals($this->msg->getOpenIDNamespace(), null); + } + + function test_getKeyOpenID() + { + $key = $this->msg->getKey(Auth_OpenID_OPENID_NS, 'foo'); + $this->assertTrue(Auth_OpenID::isFailure($key)); + + $this->msg->setOpenIDNamespace(Auth_OpenID_OPENID1_NS, false); + $key = $this->msg->getKey(Auth_OpenID_OPENID_NS, 'foo'); + $this->assertEquals('openid.foo', $key); + } + + function test_getKeyBARE() + { + $this->assertEquals($this->msg->getKey(Auth_OpenID_BARE_NS, 'foo'), 'foo'); + } + + function test_getKeyNS1() + { + $this->assertEquals($this->msg->getKey(Auth_OpenID_OPENID1_NS, 'foo'), null); + } + + function test_getKeyNS2() + { + $this->assertEquals($this->msg->getKey(Auth_OpenID_OPENID2_NS, 'foo'), null); + } + + function test_getKeyNS3() + { + $this->assertEquals($this->msg->getKey('urn:nothing-significant', 'foo'), + null); + } + + function test_hasKey() + { + $this->assertEquals($this->msg->hasKey(Auth_OpenID_OPENID_NS, 'foo'), false); + } + + function test_hasKeyBARE() + { + $this->assertEquals($this->msg->hasKey(Auth_OpenID_BARE_NS, 'foo'), false); + } + + function test_hasKeyNS1() + { + $this->assertEquals($this->msg->hasKey(Auth_OpenID_OPENID1_NS, 'foo'), false); + } + + function test_hasKeyNS2() + { + $this->assertEquals($this->msg->hasKey(Auth_OpenID_OPENID2_NS, 'foo'), false); + } + + function test_hasKeyNS3() + { + $this->assertEquals($this->msg->hasKey('urn:nothing-significant', 'foo'), + false); + } + + function test_getArg() + { + $result = $this->msg->getArg(Auth_OpenID_OPENID_NS, 'foo'); + $this->assertTrue(Auth_OpenID::isFailure($result)); + } + + function test_getArgs() + { + $result = $this->msg->getArgs(Auth_OpenID_OPENID_NS); + $this->assertTrue(Auth_OpenID::isFailure($result)); + } + + function test_getArgsBARE() + { + $this->assertEquals($this->msg->getArgs(Auth_OpenID_BARE_NS), array()); + } + + function test_getArgsNS1() + { + $this->assertEquals($this->msg->getArgs(Auth_OpenID_OPENID1_NS), array()); + } + + function test_getArgsNS2() + { + $this->assertEquals($this->msg->getArgs(Auth_OpenID_OPENID2_NS), array()); + } + + function test_getArgsNS3() + { + $this->assertEquals($this->msg->getArgs('urn:nothing-significant'), array()); + } + + function test_updateArgs() + { + $result= $this->msg->updateArgs(Auth_OpenID_OPENID_NS, + array('does not' => 'matter')); + $this->assertTrue(Auth_OpenID::isFailure($result)); + } + + function _test_updateArgsNS($ns) + { + $update_args = array( + 'Camper van Beethoven' => 'David Lowery', + 'Magnolia Electric Co.' => 'Jason Molina'); + + $this->assertEquals($this->msg->getArgs($ns), array()); + $this->msg->updateArgs($ns, $update_args); + $this->assertEquals($this->msg->getArgs($ns), $update_args); + } + + function test_updateArgsBARE() + { + $this->_test_updateArgsNS(Auth_OpenID_BARE_NS); + } + + function test_updateArgsNS1() + { + $this->_test_updateArgsNS(Auth_OpenID_OPENID1_NS); + } + + function test_updateArgsNS2() + { + $this->_test_updateArgsNS(Auth_OpenID_OPENID2_NS); + } + + function test_updateArgsNS3() + { + $this->_test_updateArgsNS('urn:nothing-significant'); + } + + function test_setArg() + { + $result = $this->msg->setArg(Auth_OpenID_OPENID_NS, + 'does not', 'matter'); + $this->assertTrue(Auth_OpenID::isFailure($result)); + } + + function _test_setArgNS($ns) + { + $key = 'Camper van Beethoven'; + $value = 'David Lowery'; + $this->assertEquals($this->msg->getArg($ns, $key), null); + $this->msg->setArg($ns, $key, $value); + $this->assertEquals($this->msg->getArg($ns, $key), $value); + } + + function test_setArgBARE() + { + $this->_test_setArgNS(Auth_OpenID_BARE_NS); + } + + function test_setArgNS1() + { + $this->_test_setArgNS(Auth_OpenID_OPENID1_NS); + } + + function test_setArgNS2() + { + $this->_test_setArgNS(Auth_OpenID_OPENID2_NS); + } + + function test_setArgNS3() + { + $this->_test_setArgNS('urn:nothing-significant'); + } + + function test_delArg() + { + $result = $this->msg->delArg(Auth_OpenID_OPENID_NS, 'does not'); + $this->assertTrue(Auth_OpenID::isFailure($result)); + } + + function _test_delArgNS($ns) + { + $key = 'Camper van Beethoven'; + $this->assertEquals($this->msg->delArg($ns, $key), false); + } + + function test_delArgBARE() + { + $this->_test_delArgNS(Auth_OpenID_BARE_NS); + } + + function test_delArgNS1() + { + $this->_test_delArgNS(Auth_OpenID_OPENID1_NS); + } + + function test_delArgNS2() + { + $this->_test_delArgNS(Auth_OpenID_OPENID2_NS); + } + + function test_delArgNS3() + { + $this->_test_delArgNS('urn:nothing-significant'); + } + + function test_isOpenID1() + { + $this->assertFalse($this->msg->isOpenID1()); + } + + function test_isOpenID2() + { + $this->assertFalse($this->msg->isOpenID2()); + } + + function test_args() + { + $this->_argTest(Auth_OpenID_BARE_NS, 'foo'); + $this->_argTest(Auth_OpenID_OPENID1_NS, 'foo'); + $this->_argTest(Auth_OpenID_OPENID2_NS, 'foo'); + $this->_argTest('urn:nothing-significant', 'foo'); + } +} + +class Tests_Auth_OpenID_OpenID1Message extends MessageTest { + function setUp() + { + $this->msg = Auth_OpenID_Message::fromPostArgs(array('openid.mode' => 'error', + 'openid.error' => 'unit test')); + } + + function test_toPostArgs() + { + $this->assertEquals($this->msg->toPostArgs(), + array('openid.mode' => 'error', + 'openid.error' => 'unit test')); + } + + function test_toArgs() + { + $this->assertEquals($this->msg->toArgs(), + array('mode' => 'error', + 'error' => 'unit test')); + } + + function test_toKVForm() + { + $this->assertEquals($this->msg->toKVForm(), + "error:unit test\nmode:error\n"); + } + + function test_toURLEncoded() + { + $this->assertEquals($this->msg->toURLEncoded(), + 'openid.error=unit+test&openid.mode=error'); + } + + function test_toURL() + { + $base_url = 'http://base.url/'; + $actual = $this->msg->toURL($base_url); + $actual_base = substr($actual, 0, strlen($base_url)); + $this->assertEquals($actual_base, $base_url); + $this->assertEquals($actual[strlen($base_url)], '?'); + $query = substr($actual, strlen($base_url) + 1); + + $parsed = Auth_OpenID::parse_str($query); + + $this->assertEquals($parsed, array('openid.mode' => 'error', + 'openid.error' => 'unit test')); + } + + function test_getOpenID() + { + $this->assertEquals($this->msg->getOpenIDNamespace(), + Auth_OpenID_OPENID1_NS); + $this->assertTrue($this->msg->namespaces->isImplicit(Auth_OpenID_OPENID1_NS)); + } + + function test_getKeyOpenID() + { + $this->assertEquals($this->msg->getKey(Auth_OpenID_OPENID_NS, 'mode'), + 'openid.mode'); + } + + function test_getKeyBARE() + { + $this->assertEquals($this->msg->getKey(Auth_OpenID_BARE_NS, 'mode'), 'mode'); + } + + function test_getKeyNS1() + { + $this->assertEquals( + $this->msg->getKey(Auth_OpenID_OPENID1_NS, 'mode'), 'openid.mode'); + } + + function test_getKeyNS2() + { + $this->assertEquals($this->msg->getKey(Auth_OpenID_OPENID2_NS, 'mode'), null); + } + + function test_getKeyNS3() + { + $this->assertEquals( + $this->msg->getKey('urn:nothing-significant', 'mode'), null); + } + + function test_hasKey() + { + $this->assertEquals($this->msg->hasKey(Auth_OpenID_OPENID_NS, 'mode'), true); + } + + function test_hasKeyBARE() + { + $this->assertEquals($this->msg->hasKey(Auth_OpenID_BARE_NS, 'mode'), false); + } + + function test_hasKeyNS1() + { + $this->assertEquals($this->msg->hasKey(Auth_OpenID_OPENID1_NS, 'mode'), true); + } + + function test_hasKeyNS2() + { + $this->assertEquals( + $this->msg->hasKey(Auth_OpenID_OPENID2_NS, 'mode'), false); + } + + function test_hasKeyNS3() + { + $this->assertEquals( + $this->msg->hasKey('urn:nothing-significant', 'mode'), false); + } + + function test_getArgs() + { + $this->assertEquals($this->msg->getArgs(Auth_OpenID_OPENID_NS), + array('mode' => 'error', + 'error' => 'unit test')); + } + + function test_getArgsBARE() + { + $this->assertEquals($this->msg->getArgs(Auth_OpenID_BARE_NS), array()); + } + + function test_getArgsNS1() + { + $this->assertEquals($this->msg->getArgs(Auth_OpenID_OPENID1_NS), + array('mode' => 'error', + 'error' => 'unit test')); + } + + function test_getArgsNS2() + { + $this->assertEquals($this->msg->getArgs(Auth_OpenID_OPENID2_NS), array()); + } + + function test_getArgsNS3() + { + $this->assertEquals($this->msg->getArgs('urn:nothing-significant'), array()); + } + + function _test_updateArgsNS($ns, $before=null) + { + if ($before === null) { + $before = array(); + } + + $update_args = array( + 'Camper van Beethoven' => 'David Lowery', + 'Magnolia Electric Co.' => 'Jason Molina'); + + $this->assertEquals($this->msg->getArgs($ns), $before); + $this->msg->updateArgs($ns, $update_args); + $after = $before; + $after = array_merge($after, $update_args); + $this->assertEquals($this->msg->getArgs($ns), $after); + } + + function test_updateArgs() + { + $this->_test_updateArgsNS(Auth_OpenID_OPENID_NS, + array('mode' => 'error', 'error' => 'unit test')); + } + + function test_updateArgsBARE() + { + $this->_test_updateArgsNS(Auth_OpenID_BARE_NS); + } + + function test_updateArgsNS1() + { + $this->_test_updateArgsNS(Auth_OpenID_OPENID1_NS, + array('mode' => 'error', 'error' => 'unit test')); + } + + function test_updateArgsNS2() + { + $this->_test_updateArgsNS(Auth_OpenID_OPENID2_NS); + } + + function test_updateArgsNS3() + { + $this->_test_updateArgsNS('urn:nothing-significant'); + } + + function _test_setArgNS($ns) + { + $key = 'Camper van Beethoven'; + $value = 'David Lowery'; + $this->assertEquals($this->msg->getArg($ns, $key), null); + $this->msg->setArg($ns, $key, $value); + $this->assertEquals($this->msg->getArg($ns, $key), $value); + } + + function test_setArg() + { + $this->_test_setArgNS(Auth_OpenID_OPENID_NS); + } + + function test_setArgBARE() + { + $this->_test_setArgNS(Auth_OpenID_BARE_NS); + } + + function test_setArgNS1() + { + $this->_test_setArgNS(Auth_OpenID_OPENID1_NS); + } + + function test_setArgNS2() + { + $this->_test_setArgNS(Auth_OpenID_OPENID2_NS); + } + + function test_setArgNS3() + { + $this->_test_setArgNS('urn:nothing-significant'); + } + + function _test_delArgNS($ns) + { + $key = 'Camper van Beethoven'; + $value = 'David Lowery'; + + $this->assertEquals($this->msg->delArg($ns, $key), false); + $this->msg->setArg($ns, $key, $value); + $this->assertEquals($this->msg->getArg($ns, $key), $value); + $this->msg->delArg($ns, $key); + $this->assertEquals($this->msg->getArg($ns, $key), null); + } + + function test_delArg() + { + $this->_test_delArgNS(Auth_OpenID_OPENID_NS); + } + + function test_delArgBARE() + { + $this->_test_delArgNS(Auth_OpenID_BARE_NS); + } + + function test_delArgNS1() + { + $this->_test_delArgNS(Auth_OpenID_OPENID1_NS); + } + + function test_delArgNS2() + { + $this->_test_delArgNS(Auth_OpenID_OPENID2_NS); + } + + function test_delArgNS3() + { + $this->_test_delArgNS('urn:nothing-significant'); + } + + function test_isOpenID1() + { + $this->assertTrue($this->msg->isOpenID1()); + } + + function test_isOpenID2() + { + $this->assertFalse($this->msg->isOpenID2()); + } + + function test_args() + { + $this->_argTest(Auth_OpenID_BARE_NS, 'mode'); + $this->_argTest(Auth_OpenID_OPENID_NS, 'mode', 'error'); + $this->_argTest(Auth_OpenID_OPENID1_NS, 'mode', 'error'); + $this->_argTest(Auth_OpenID_OPENID2_NS, 'mode'); + $this->_argTest('urn:nothing-significant', 'mode'); + } +} + +class Tests_Auth_OpenID_OpenID1ExplicitMessage extends PHPUnit_TestCase { + function setUp() + { + $this->msg = Auth_OpenID_Message::fromPostArgs(array('openid.mode' => 'error', + 'openid.error' => 'unit test', + 'openid.ns' => Auth_OpenID_OPENID1_NS)); + } + + function test_isOpenID1() + { + $this->assertTrue($this->msg->isOpenID1()); + $this->assertFalse( + $this->msg->namespaces->isImplicit(Auth_OpenID_OPENID1_NS)); + } + + function test_isOpenID2() + { + $this->assertFalse($this->msg->isOpenID2()); + } + + function test_toPostArgs() + { + $this->assertEquals($this->msg->toPostArgs(), + array('openid.mode' => 'error', + 'openid.error' => 'unit test', + 'openid.ns' => Auth_OpenID_OPENID1_NS)); + } + + function test_toArgs() + { + $this->assertEquals($this->msg->toArgs(), + array('mode' => 'error', + 'error' => 'unit test', + 'ns' => Auth_OpenID_OPENID1_NS)); + } + + function test_toKVForm() + { + $this->assertEquals($this->msg->toKVForm(), + "error:unit test\nmode:error\nns:". + Auth_OpenID_OPENID1_NS."\n"); + } + + function test_toURLEncoded() + { + $this->assertEquals($this->msg->toURLEncoded(), + 'openid.error=unit+test&openid.mode=error&openid.ns=http%3A%2F%2Fopenid.net%2Fsignon%2F1.0'); + } + + function test_toURL() + { + $base_url = 'http://base.url/'; + $actual = $this->msg->toURL($base_url); + $actual_base = substr($actual, 0, strlen($base_url)); + $this->assertEquals($actual_base, $base_url); + $this->assertEquals($actual[strlen($base_url)], '?'); + $query = substr($actual, strlen($base_url) + 1); + + $parsed = Auth_OpenID::parse_str($query); + + $this->assertEquals($parsed, array('openid.mode' => 'error', + 'openid.error' => 'unit test', + 'openid.ns' => Auth_OpenID_OPENID1_NS)); + } +} + +class Tests_Auth_OpenID_OpenID2Message extends MessageTest { + function setUp() + { + $this->msg = Auth_OpenID_Message::fromPostArgs(array('openid.mode' => 'error', + 'openid.error' => 'unit test', + 'openid.ns' => Auth_OpenID_OPENID2_NS)); + $this->msg->setArg(Auth_OpenID_BARE_NS, "xey", "value"); + } + + function test_toPostArgs() + { + $this->assertEquals($this->msg->toPostArgs(), + array('openid.mode' => 'error', + 'openid.error' => 'unit test', + 'openid.ns' => Auth_OpenID_OPENID2_NS, + 'xey' => 'value')); + } + + function test_toArgs() + { + // This method can't tolerate BARE_NS. + $this->msg->delArg(Auth_OpenID_BARE_NS, "xey"); + $this->assertEquals($this->msg->toArgs(), + array('mode' => 'error', + 'error' => 'unit test', + 'ns' => Auth_OpenID_OPENID2_NS)); + } + + function test_toKVForm() + { + // Can't tolerate BARE_NS in kvform + $this->msg->delArg(Auth_OpenID_BARE_NS, "xey"); + $this->assertEquals($this->msg->toKVForm(), + sprintf("error:unit test\nmode:error\nns:%s\n", + Auth_OpenID_OPENID2_NS)); + } + + function _test_urlencoded($s) + { + $expected = 'openid.error=unit+test&openid.mode=error&' . + 'openid.ns=%s&xey=value'; + + $expected = sprintf($expected, urlencode(Auth_OpenID_OPENID2_NS)); + $this->assertEquals($s, $expected); + } + + function test_toURLEncoded() + { + $this->_test_urlencoded($this->msg->toURLEncoded()); + } + + function test_toURL() + { + $base_url = 'http://base.url/'; + $actual = $this->msg->toURL($base_url); + $actual_base = substr($actual, 0, strlen($base_url)); + + $this->assertEquals($actual_base, $base_url); + $this->assertEquals($actual[strlen($base_url)], '?'); + $query = substr($actual, strlen($base_url) + 1); + $this->_test_urlencoded($query); + } + + function test_getOpenID() + { + $this->assertEquals($this->msg->getOpenIDNamespace(), + Auth_OpenID_OPENID2_NS); + } + + function test_getKeyOpenID() + { + $this->assertEquals($this->msg->getKey(Auth_OpenID_OPENID_NS, 'mode'), + 'openid.mode'); + } + + function test_getKeyBARE() + { + $this->assertEquals($this->msg->getKey(Auth_OpenID_BARE_NS, 'mode'), 'mode'); + } + + function test_getKeyNS1() + { + $this->assertEquals( + $this->msg->getKey(Auth_OpenID_OPENID1_NS, 'mode'), null); + } + + function test_getKeyNS2() + { + $this->assertEquals( + $this->msg->getKey(Auth_OpenID_OPENID2_NS, 'mode'), 'openid.mode'); + } + + function test_getKeyNS3() + { + $this->assertEquals( + $this->msg->getKey('urn:nothing-significant', 'mode'), null); + } + + function test_hasKeyOpenID() + { + $this->assertEquals($this->msg->hasKey(Auth_OpenID_OPENID_NS, 'mode'), true); + } + + function test_hasKeyBARE() + { + $this->assertEquals($this->msg->hasKey(Auth_OpenID_BARE_NS, 'mode'), false); + } + + function test_hasKeyNS1() + { + $this->assertEquals( + $this->msg->hasKey(Auth_OpenID_OPENID1_NS, 'mode'), false); + } + + function test_hasKeyNS2() + { + $this->assertEquals( + $this->msg->hasKey(Auth_OpenID_OPENID2_NS, 'mode'), true); + } + + function test_hasKeyNS3() + { + $this->assertEquals( + $this->msg->hasKey('urn:nothing-significant', 'mode'), false); + } + + function test_getArgsOpenID() + { + $this->assertEquals($this->msg->getArgs(Auth_OpenID_OPENID_NS), + array('mode' => 'error', + 'error' => 'unit test')); + } + + function test_getArgsBARE() + { + $this->assertEquals($this->msg->getArgs(Auth_OpenID_BARE_NS), + array('xey' => 'value')); + } + + function test_getArgsNS1() + { + $this->assertEquals($this->msg->getArgs(Auth_OpenID_OPENID1_NS), array()); + } + + function test_getArgsNS2() + { + $this->assertEquals($this->msg->getArgs(Auth_OpenID_OPENID2_NS), + array('mode' => 'error', + 'error' => 'unit test')); + } + + function test_getArgsNS3() + { + $this->assertEquals($this->msg->getArgs('urn:nothing-significant'), array()); + } + + function _test_updateArgsNS($ns, $before=null) + { + if ($before === null) { + $before = array(); + } + + $update_args = array( + 'Camper van Beethoven' => 'David Lowery', + 'Magnolia Electric Co.' => 'Jason Molina'); + + $this->assertEquals($this->msg->getArgs($ns), $before); + $this->msg->updateArgs($ns, $update_args); + $after = $before; + $after = array_merge($after, $update_args); + $this->assertEquals($this->msg->getArgs($ns), $after); + } + + function test_updateArgsOpenID() + { + $this->_test_updateArgsNS(Auth_OpenID_OPENID_NS, + array('mode' => 'error', 'error' => 'unit test')); + } + + function test_updateArgsBARE() + { + $this->_test_updateArgsNS(Auth_OpenID_BARE_NS, + array('xey' => 'value')); + } + + function test_updateArgsNS1() + { + $this->_test_updateArgsNS(Auth_OpenID_OPENID1_NS); + } + + function test_updateArgsNS2() + { + $this->_test_updateArgsNS(Auth_OpenID_OPENID2_NS, + array('mode' => 'error', 'error' => 'unit test')); + } + + function test_updateArgsNS3() + { + $this->_test_updateArgsNS('urn:nothing-significant'); + } + + function _test_setArgNS($ns) + { + $key = 'Camper van Beethoven'; + $value = 'David Lowery'; + $this->assertEquals($this->msg->getArg($ns, $key), null); + $this->msg->setArg($ns, $key, $value); + $this->assertEquals($this->msg->getArg($ns, $key), $value); + } + + function test_setArgOpenID() + { + $this->_test_setArgNS(Auth_OpenID_OPENID_NS); + } + + function test_setArgBARE() + { + $this->_test_setArgNS(Auth_OpenID_BARE_NS); + } + + function test_setArgNS1() + { + $this->_test_setArgNS(Auth_OpenID_OPENID1_NS); + } + + function test_setArgNS2() + { + $this->_test_setArgNS(Auth_OpenID_OPENID2_NS); + } + + function test_setArgNS3() + { + $this->_test_setArgNS('urn:nothing-significant'); + } + + function test_badAlias() + { + // Make sure dotted aliases and OpenID protocol fields are not + // allowed as namespace aliases. + + global $Auth_OpenID_OPENID_PROTOCOL_FIELDS; + + $all = array_merge($Auth_OpenID_OPENID_PROTOCOL_FIELDS, array('dotted.alias')); + + foreach ($all as $f) { + $args = array(sprintf('openid.ns.%s', $f) => 'blah', + sprintf('openid.%s.foo', $f) => 'test'); + + // .fromPostArgs covers .fromPostArgs, .fromOpenIDArgs, + // ._fromOpenIDArgs, and .fromOpenIDArgs (since it calls + // .fromPostArgs). Python code raises AssertionError, but + // we usually return null for bad things in PHP. + $this->assertEquals($this->msg->fromPostArgs($args), null); + } + } + + function _test_delArgNS($ns) + { + $key = 'Camper van Beethoven'; + $value = 'David Lowery'; + + $this->assertEquals($this->msg->delArg($ns, $key), false); + $this->msg->setArg($ns, $key, $value); + $this->assertEquals($this->msg->getArg($ns, $key), $value); + $this->msg->delArg($ns, $key); + $this->assertEquals($this->msg->getArg($ns, $key), null); + } + + function test_delArgOpenID() + { + $this->_test_delArgNS(Auth_OpenID_OPENID_NS); + } + + function test_delArgBARE() + { + $this->_test_delArgNS(Auth_OpenID_BARE_NS); + } + + function test_delArgNS1() + { + $this->_test_delArgNS(Auth_OpenID_OPENID1_NS); + } + + function test_delArgNS2() + { + $this->_test_delArgNS(Auth_OpenID_OPENID2_NS); + } + + function test_delArgNS3() + { + $this->_test_delArgNS('urn:nothing-significant'); + } + + function test_overwriteExtensionArg() + { + $ns = 'urn:unittest_extension'; + $key = 'mykey'; + $value_1 = 'value_1'; + $value_2 = 'value_2'; + + $this->msg->setArg($ns, $key, $value_1); + $this->assertTrue($this->msg->getArg($ns, $key) == $value_1); + $this->msg->setArg($ns, $key, $value_2); + $this->assertTrue($this->msg->getArg($ns, $key) == $value_2); + } + + function test_argList() + { + $this->assertEquals($this->msg->fromPostArgs(array('arg' => array(1, 2, 3))), + null); + } + + function test_isOpenID1() + { + $this->assertFalse($this->msg->isOpenID1()); + } + + function test_isOpenID2() + { + $this->assertTrue($this->msg->isOpenID2()); + } + + function test_args() + { + $this->_argTest(Auth_OpenID_BARE_NS, 'mode'); + $this->_argTest(Auth_OpenID_OPENID_NS, 'mode', 'error'); + $this->_argTest(Auth_OpenID_OPENID1_NS, 'mode'); + $this->_argTest(Auth_OpenID_OPENID2_NS, 'mode', 'error'); + $this->_argTest('urn:nothing-significant', 'mode'); + } +} + +class Tests_Auth_OpenID_GeneralMessageTest extends PHPUnit_TestCase { + function setUp() + { + $this->postargs = array( + 'openid.ns' => Auth_OpenID_OPENID2_NS, + 'openid.mode' => 'checkid_setup', + 'openid.identity' => 'http://bogus.example.invalid:port/', + 'openid.assoc_handle' => 'FLUB', + 'openid.return_to' => 'Neverland'); + + $this->action_url = 'scheme://host:port/path?query'; + + $this->form_tag_attrs = array( + 'company' => 'janrain', + 'class' => 'fancyCSS'); + + $this->submit_text = 'GO!'; + + // Expected data regardless of input + + $this->required_form_attrs = array( + 'accept-charset' => 'UTF-8', + 'enctype' => 'application/x-www-form-urlencoded', + 'method' => 'post'); + } + + function _checkForm($html, $message_, $action_url, + $form_tag_attrs, $submit_text) + { + $parser =& Auth_Yadis_getXMLParser(); + + // Parse HTML source + $this->assertTrue($parser->init($html, array())); + + // Get root element + $form = $parser->evalXPath('/form[1]'); + $this->assertTrue(count($form) == 1); + $form = $form[0]; + + // Check required form attributes + $form_attrs = $parser->attributes($form); + foreach ($this->required_form_attrs as $k => $v) { + $this->assertTrue($form_attrs[$k] == $v); + } + + // Check extra form attributes + foreach ($form_tag_attrs as $k => $v) { + // Skip attributes that already passed the required + // attribute check, since they should be ignored by the + // form generation code. + if (in_array($k, array_keys($this->required_form_attrs))) { + continue; + } + + $this->assertTrue($form_attrs[$k] == $v, + "Form attr $k is ".$form_attrs[$k]." (expected $v)"); + } + + // Check hidden fields against post args + $hiddens = array(); + $input_elements = $parser->evalXPath('input', $form); + foreach ($input_elements as $e) { + $attrs = $parser->attributes($e); + if (strtoupper($attrs['type']) == 'HIDDEN') { + $hiddens[] = $e; + } + } + + // For each post arg, make sure there is a hidden with that + // value. Make sure there are no other hiddens. + $postargs = $message_->toPostArgs(); + foreach ($postargs as $name => $value) { + $found = false; + + foreach ($hiddens as $e) { + $attrs = $parser->attributes($e); + if ($attrs['name'] == $name) { + $this->assertTrue($attrs['value'] == $value); + $found = true; + break; + } + } + + if (!$found) { + $this->fail("Post arg $name not found in form"); + } + } + + $keys = array_keys($postargs); + foreach ($hiddens as $e) { + $attrs = $parser->attributes($e); + $this->assertTrue(in_array($attrs['name'], $keys)); + } + + // Check action URL + $this->assertTrue($form_attrs['action'] == $action_url); + + // Check submit text + $submits = array(); + foreach ($input_elements as $e) { + $attrs = $parser->attributes($e); + if (strtoupper($attrs['type']) == 'SUBMIT') { + $submits[] = $e; + } + } + + $this->assertTrue(count($submits) == 1); + + $attrs = $parser->attributes($submits[0]); + $this->assertTrue($attrs['value'] == $submit_text); + } + + function test_toFormMarkup() + { + $m = Auth_OpenID_Message::fromPostArgs($this->postargs); + $html = $m->toFormMarkup($this->action_url, $this->form_tag_attrs, + $this->submit_text); + $this->_checkForm($html, $m, $this->action_url, + $this->form_tag_attrs, $this->submit_text); + } + + function test_overrideMethod() + { + // Be sure that caller cannot change form method to GET. + $m = Auth_OpenID_Message::fromPostArgs($this->postargs); + + $tag_attrs = $this->form_tag_attrs; + $tag_attrs['method'] = 'GET'; + + $html = $m->toFormMarkup($this->action_url, $this->form_tag_attrs, + $this->submit_text); + $this->_checkForm($html, $m, $this->action_url, + $this->form_tag_attrs, $this->submit_text); + } + + function test_overrideRequired() + { + // Be sure that caller CANNOT change the form charset for + // encoding type. + $m = Auth_OpenID_Message::fromPostArgs($this->postargs); + + $tag_attrs = $this->form_tag_attrs; + $tag_attrs['accept-charset'] = 'UCS4'; + $tag_attrs['enctype'] = 'invalid/x-broken'; + + $html = $m->toFormMarkup($this->action_url, $tag_attrs, + $this->submit_text); + $this->_checkForm($html, $m, $this->action_url, + $tag_attrs, $this->submit_text); + } + + function test_setOpenIDNamespace_invalid() + { + $m = new Auth_OpenID_Message(); + $invalid_things = array( + // Empty string is not okay here. + '', + // Good guess! But wrong. + 'http://openid.net/signon/2.0', + // What? + 'http://specs%\\\r2Eopenid.net/auth/2.0', + // Too much escapings! + 'http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0', + // This is a Type URI, not a openid.ns value. + 'http://specs.openid.net/auth/2.0/signon', + ); + + foreach ($invalid_things as $x) { + $this->assertTrue($m->setOpenIDNamespace($x, true) === false); + } + } + + function test_isOpenID1() + { + $v1_namespaces = array( + // Yes, there are two of them. + 'http://openid.net/signon/1.1', + 'http://openid.net/signon/1.0', + ); + + foreach ($v1_namespaces as $ns) { + $m = new Auth_OpenID_Message($ns); + $this->assertTrue($m->isOpenID1(), + "$ns not recognized as OpenID 1"); + $this->assertEquals($ns, $m->getOpenIDNamespace()); + $this->assertTrue($m->namespaces->isImplicit($ns)); + } + } + + function test_isOpenID2() + { + $ns = 'http://specs.openid.net/auth/2.0'; + $m = new Auth_OpenID_Message($ns); + $this->assertTrue($m->isOpenID2()); + $this->assertFalse( + $m->namespaces->isImplicit(Auth_OpenID_NULL_NAMESPACE)); + $this->assertEquals($ns, $m->getOpenIDNamespace()); + } + + function test_setOpenIDNamespace_explicit() + { + $m = new Auth_OpenID_Message(); + $m->setOpenIDNamespace(Auth_OpenID_THE_OTHER_OPENID1_NS, false); + $this->assertFalse($m->namespaces->isImplicit( + Auth_OpenID_THE_OTHER_OPENID1_NS)); + } + + function test_setOpenIDNamespace_implicit() + { + $m = new Auth_OpenID_Message(); + $m->setOpenIDNamespace(Auth_OpenID_THE_OTHER_OPENID1_NS, true); + $this->assertTrue( + $m->namespaces->isImplicit(Auth_OpenID_THE_OTHER_OPENID1_NS)); + } + + + function test_explicitOpenID11NSSerialzation() + { + $m = new Auth_OpenID_Message(); + $m->setOpenIDNamespace(Auth_OpenID_THE_OTHER_OPENID1_NS, false); + + $post_args = $m->toPostArgs(); + $this->assertEquals($post_args, + array('openid.ns' => + Auth_OpenID_THE_OTHER_OPENID1_NS)); + } + + function test_fromPostArgs_ns11() + { + // An example of the stuff that some Drupal installations send us, + // which includes openid.ns but is 1.1. + $query = array( + 'openid.assoc_handle' => '', + 'openid.claimed_id' => 'http://foobar.invalid/', + 'openid.identity' => 'http://foobar.myopenid.com', + 'openid.mode' => 'checkid_setup', + 'openid.ns' => 'http://openid.net/signon/1.1', + 'openid.ns.sreg' => 'http://openid.net/extensions/sreg/1.1', + 'openid.return_to' => 'http://drupal.invalid/return_to', + 'openid.sreg.required' => 'nickname,email', + 'openid.trust_root' => 'http://drupal.invalid', + ); + $m = Auth_OpenID_Message::fromPostArgs($query); + $this->assertTrue($m->isOpenID1()); + } +} + +class Tests_Auth_OpenID_NamespaceMap extends PHPUnit_TestCase { + function test_onealias() + { + $nsm = new Auth_OpenID_NamespaceMap(); + $uri = 'http://example.com/foo'; + $alias = "foo"; + $nsm->addAlias($uri, $alias); + $this->assertTrue($nsm->getNamespaceURI($alias) == $uri); + $this->assertTrue($nsm->getAlias($uri) == $alias); + } + + function test_iteration() + { + $nsm = new Auth_OpenID_NamespaceMap(); + $uripat = 'http://example.com/foo%d'; + + $nsm->add(sprintf($uripat, 0)); + + for ($n = 1; $n < 23; $n++) { + $this->assertTrue($nsm->contains(sprintf($uripat, $n - 1))); + $this->assertTrue($nsm->isDefined(sprintf($uripat, $n - 1))); + $nsm->add(sprintf($uripat, $n)); + } + + foreach ($nsm->iteritems() as $pair) { + list($uri, $alias) = $pair; + $this->assertTrue('ext'.substr($uri, 22) == $alias); + } + + $it = $nsm->iterAliases(); + $this->assertTrue(count($it) == 23); + + $it = $nsm->iterNamespaceURIs(); + $this->assertTrue(count($it) == 23); + } +} + +class Tests_Auth_OpenID_Message extends PHPUnit_TestCase { +} + +global $Tests_Auth_OpenID_Message_other; +$Tests_Auth_OpenID_Message_other = array( + new Tests_Auth_OpenID_EmptyMessage(), + new Tests_Auth_OpenID_OpenID1Message(), + new Tests_Auth_OpenID_OpenID2Message(), + new Tests_Auth_OpenID_NamespaceMap(), + new Tests_Auth_OpenID_OpenID1ExplicitMessage(), + new Tests_Auth_OpenID_GeneralMessageTest() + ); + +?> diff --git a/Tests/Auth/OpenID/Negotiation.php b/Tests/Auth/OpenID/Negotiation.php new file mode 100644 index 0000000..c5ad873 --- /dev/null +++ b/Tests/Auth/OpenID/Negotiation.php @@ -0,0 +1,348 @@ +<?php + +require_once "PHPUnit.php"; +require_once "Tests/Auth/OpenID/TestUtil.php"; +require_once "Tests/Auth/OpenID/MemStore.php"; + +require_once "Auth/OpenID/Message.php"; +require_once "Auth/OpenID/Consumer.php"; + +/** + * A consumer whose _requestAssocation will return predefined results + * instead of trying to actually perform association requests. + */ +class ErrorRaisingConsumer extends Auth_OpenID_GenericConsumer { + // The list of objects to be returned by successive calls to + // _requestAssocation. Each call will pop the first element from + // this list and return it to _negotiateAssociation. If the + // element is a Message object, it will be wrapped in a + // ServerErrorContainer exception. Otherwise it will be returned + // as-is. + var $return_messages = array(); + + function _requestAssociation($endpoint, $assoc_type, $session_type) + { + $m = array_pop($this->return_messages); + if (is_a($m, 'Auth_OpenID_Message')) { + return Auth_OpenID_ServerErrorContainer::fromMessage($m); + } else if (Auth_OpenID::isFailure($m)) { + return $m; + } else { + return $m; + } + } +} + +/** + * Test the session type negotiation behavior of an OpenID 2 consumer. + */ +class TestOpenID2SessionNegotiation extends PHPUnit_TestCase { + function setUp() + { + $dumb = null; + $this->consumer = new ErrorRaisingConsumer($dumb); + $this->endpoint = new Auth_OpenID_ServiceEndpoint(); + $this->endpoint->type_uris = array(Auth_OpenID_TYPE_2_0); + $this->endpoint->server_url = 'bogus'; + } + + /** + * Test the case where the response to an associate request is a + * server error or is otherwise undecipherable. + */ + function testBadResponse() + { + $this->consumer->return_messages = array( + new Auth_OpenID_Message($this->endpoint->preferredNamespace())); + $this->assertEquals($this->consumer->_negotiateAssociation($this->endpoint), null); + // $this->failUnlessLogMatches('Server error when requesting an association') + } + + /** + * Test the case where the response to an associate request is a + * a failure response object. + */ + function testBadResponseWithFailure() + { + $this->consumer->return_messages = array( + new Auth_OpenID_FailureResponse($this->endpoint)); + $this->assertEquals($this->consumer->_negotiateAssociation($this->endpoint), null); + // $this->failUnlessLogMatches('Server error when requesting an association') + } + + /** + * Test the case where the association type (assoc_type) returned + * in an unsupported-type response is absent. + */ + function testEmptyAssocType() + { + $msg = new Auth_OpenID_Message($this->endpoint->preferredNamespace()); + $msg->setArg(Auth_OpenID_OPENID_NS, 'error', 'Unsupported type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'error_code', 'unsupported-type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'assoc_type', null); + $msg->setArg(Auth_OpenID_OPENID_NS, 'session_type', 'new-session-type'); + + $this->consumer->return_messages = array($msg); + $this->assertEquals($this->consumer->_negotiateAssociation($this->endpoint), null); + + // $this->failUnlessLogMatches('Unsupported association type', + // 'Server responded with unsupported association ' + + // 'session but did not supply a fallback.') + } + + /** + * Test the case where the session type (session_type) returned in + * an unsupported-type response is absent. + */ + function testEmptySessionType() + { + $msg = new Auth_OpenID_Message($this->endpoint->preferredNamespace()); + $msg->setArg(Auth_OpenID_OPENID_NS, 'error', 'Unsupported type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'error_code', 'unsupported-type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'assoc_type', 'new-assoc-type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'session_type', null); + + $this->consumer->return_messages = array($msg); + $this->assertEquals($this->consumer->_negotiateAssociation($this->endpoint), null); + + // $this->failUnlessLogMatches('Unsupported association type', + // 'Server responded with unsupported association ' + + // 'session but did not supply a fallback.') + } + + /** + * Test the case where an unsupported-type response specifies a + * preferred (assoc_type, session_type) combination that is not + * allowed by the consumer's SessionNegotiator. + */ + function testNotAllowed() + { + $allowed_types = array(); + + $negotiator = new Auth_OpenID_SessionNegotiator($allowed_types); + $this->consumer->negotiator = $negotiator; + + $msg = new Auth_OpenID_Message($this->endpoint->preferredNamespace()); + $msg->setArg(Auth_OpenID_OPENID_NS, 'error', 'Unsupported type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'error_code', 'unsupported-type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'assoc_type', 'not-allowed'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'session_type', 'not-allowed'); + + $this->consumer->return_messages = array($msg); + $this->assertEquals($this->consumer->_negotiateAssociation($this->endpoint), null); + + // $this->failUnlessLogMatches('Unsupported association type', + // 'Server sent unsupported session/association type:') + } + + /** + * Test the case where an unsupported-type response triggers a + * retry to get an association with the new preferred type. + */ + function testUnsupportedWithRetry() + { + $msg = new Auth_OpenID_Message($this->endpoint->preferredNamespace()); + $msg->setArg(Auth_OpenID_OPENID_NS, 'error', 'Unsupported type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'error_code', 'unsupported-type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'assoc_type', 'HMAC-SHA1'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'session_type', 'DH-SHA1'); + + $assoc = new Auth_OpenID_Association( + 'handle', 'secret', 'issued', 10000, 'HMAC-SHA1'); + + $this->consumer->return_messages = array($msg, $assoc); + $this->assertTrue($this->consumer->_negotiateAssociation($this->endpoint) === $assoc); + + // $this->failUnlessLogMatches('Unsupported association type'); + } + + /** + * Test the case where an unsupported-typ response triggers a + * retry, but the retry fails and None is returned instead. + */ + function testUnsupportedWithRetryAndFail() + { + $msg = new Auth_OpenID_Message($this->endpoint->preferredNamespace()); + $msg->setArg(Auth_OpenID_OPENID_NS, 'error', 'Unsupported type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'error_code', 'unsupported-type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'assoc_type', 'HMAC-SHA1'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'session_type', 'DH-SHA1'); + + $this->consumer->return_messages = array($msg, + new Auth_OpenID_Message($this->endpoint->preferredNamespace())); + + $this->assertEquals($this->consumer->_negotiateAssociation($this->endpoint), null); + + // $this->failUnlessLogMatches('Unsupported association type', + // 'Server %s refused' % ($this->endpoint.server_url)) + } + + /** + * Test the valid case, wherein an association is returned on the + * first attempt to get one. + */ + function testValid() + { + $assoc = new Auth_OpenID_Association( + 'handle', 'secret', 'issued', 10000, 'HMAC-SHA1'); + + $this->consumer->return_messages = array($assoc); + $this->assertTrue($this->consumer->_negotiateAssociation($this->endpoint) === $assoc); + // $this->failUnlessLogEmpty() + } +} + +/** + * Tests for the OpenID 1 consumer association session behavior. See + * the docs for TestOpenID2SessionNegotiation. Notice that this class + * is not a subclass of the OpenID 2 tests. Instead, it uses many of + * the same inputs but inspects the log messages logged with + * oidutil.log. See the calls to $this->failUnlessLogMatches. Some + * of these tests pass openid2-style messages to the openid 1 + * association processing logic to be sure it ignores the extra data. + */ +class TestOpenID1SessionNegotiation extends PHPUnit_TestCase { + function setUp() + { + $dumb = null; + $this->consumer = new ErrorRaisingConsumer($dumb); + + $this->endpoint = new Auth_OpenID_ServiceEndpoint(); + $this->endpoint->type_uris = array(Auth_OpenID_OPENID1_NS); + $this->endpoint->server_url = 'bogus'; + } + + function testBadResponse() + { + $this->consumer->return_messages = + array(new Auth_OpenID_Message($this->endpoint->preferredNamespace())); + $this->assertEquals($this->consumer->_negotiateAssociation($this->endpoint), null); + // $this->failUnlessLogMatches('Server error when requesting an association') + } + + function testEmptyAssocType() + { + $msg = new Auth_OpenID_Message($this->endpoint->preferredNamespace()); + $msg->setArg(Auth_OpenID_OPENID_NS, 'error', 'Unsupported type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'error_code', 'unsupported-type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'assoc_type', null); + $msg->setArg(Auth_OpenID_OPENID_NS, 'session_type', 'new-session-type'); + + $this->consumer->return_messages = array($msg); + $this->assertEquals($this->consumer->_negotiateAssociation($this->endpoint), null); + + // $this->failUnlessLogMatches('Server error when requesting an association') + } + + function testEmptySessionType() + { + $msg = new Auth_OpenID_Message($this->endpoint->preferredNamespace()); + $msg->setArg(Auth_OpenID_OPENID_NS, 'error', 'Unsupported type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'error_code', 'unsupported-type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'assoc_type', 'new-assoc-type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'session_type', null); + + $this->consumer->return_messages = array($msg); + $this->assertEquals($this->consumer->_negotiateAssociation($this->endpoint), null); + + // $this->failUnlessLogMatches('Server error when requesting an association'); + } + + function testNotAllowed() + { + $allowed_types = array(); + + $negotiator = new Auth_OpenID_SessionNegotiator($allowed_types); + $this->consumer->negotiator = $negotiator; + + $msg = new Auth_OpenID_Message($this->endpoint->preferredNamespace()); + $msg->setArg(Auth_OpenID_OPENID_NS, 'error', 'Unsupported type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'error_code', 'unsupported-type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'assoc_type', 'not-allowed'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'session_type', 'not-allowed'); + + $this->consumer->return_messages = array($msg); + $this->assertEquals($this->consumer->_negotiateAssociation($this->endpoint), null); + + // $this->failUnlessLogMatches('Server error when requesting an association') + } + + function testUnsupportedWithRetry() + { + $msg = new Auth_OpenID_Message($this->endpoint->preferredNamespace()); + $msg->setArg(Auth_OpenID_OPENID_NS, 'error', 'Unsupported type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'error_code', 'unsupported-type'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'assoc_type', 'HMAC-SHA1'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'session_type', 'DH-SHA1'); + + $assoc = new Auth_OpenID_Association( + 'handle', 'secretxx', 'issued', 10000, 'HMAC-SHA1'); + + $this->consumer->return_messages = array($assoc, $msg); + + $result = $this->consumer->_negotiateAssociation($this->endpoint); + $this->assertTrue($result === null); + + // $this->failUnlessLogMatches('Server error when requesting an association') + } + + function testValid() + { + $assoc = new Auth_OpenID_Association( + 'handle', 'secret', 'issued', 10000, 'HMAC-SHA1'); + + $this->consumer->return_messages = array($assoc); + $this->assertTrue($this->consumer->_negotiateAssociation($this->endpoint) === $assoc); + // $this->failUnlessLogEmpty() + } +} + +class TestNegotiatorBehaviors extends PHPUnit_TestCase { + function setUp() + { + $this->allowed_types = array( + array('HMAC-SHA1', 'no-encryption'), + array('HMAC-SHA256', 'no-encryption') + ); + + $this->n = new Auth_OpenID_SessionNegotiator($this->allowed_types); + } + + function testAddAllowedTypeNoSessionTypes() + { + $this->assertFalse($this->n->addAllowedType('invalid')); + } + + function testAddAllowedTypeBadSessionType() + { + $this->assertFalse($this->n->addAllowedType('assoc1', 'invalid')); + } + + function testAddAllowedTypeContents() + { + $assoc_type = 'HMAC-SHA1'; + $this->assertTrue($this->n->addAllowedType($assoc_type)); + + foreach (Auth_OpenID_getSessionTypes($assoc_type) as $typ) { + $this->assertTrue(in_array(array($assoc_type, $typ), + $this->n->allowed_types)); + } + } +} + +class Tests_Auth_OpenID_Negotiation extends PHPUnit_TestSuite { + + function getName() + { + return 'Tests_Auth_OpenID_Negotiation'; + } + + function Tests_Auth_OpenID_Negotiation() + { + $this->addTestSuite('TestNegotiatorBehaviors'); + $this->addTestSuite('TestOpenID1SessionNegotiation'); + $this->addTestSuite('TestOpenID2SessionNegotiation'); + } +} + +?>
\ No newline at end of file diff --git a/Tests/Auth/OpenID/Nonce.php b/Tests/Auth/OpenID/Nonce.php new file mode 100644 index 0000000..1936735 --- /dev/null +++ b/Tests/Auth/OpenID/Nonce.php @@ -0,0 +1,167 @@ +<?php + +/** + * Tests for the Nonce implementation. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2006 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'PHPUnit.php'; +require_once 'Auth/OpenID/Nonce.php'; + +define('Tests_Auth_OpenID_nonce_re', + '/\A\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ/'); + +class Tests_Auth_OpenID_Nonce extends PHPUnit_TestSuite { + function Tests_Auth_OpenID_Nonce() + { + $this->addTestSuite('Tests_Auth_OpenID_NonceTests'); + $this->makeSplitTests(); + $this->makeCheckTimestampTests(); + $this->setName('Tests_Auth_OpenID_Nonce'); + } + + function makeSplitTests() + { + $cases = array( + '', + '1970-01-01T00:00:00+1:00', + '1969-01-01T00:00:00Z', + '1970-00-01T00:00:00Z', + '1970.01-01T00:00:00Z', + 'Thu Sep 7 13:29:31 PDT 2006', + 'monkeys', + ); + + foreach ($cases as $nonce_str) { + $this->_mkSplitTest($nonce_str); + } + } + + function _mkSplitTest($nonce_str) + { + $test = new Tests_Auth_OpenID_Nonce_BadSplitCase($nonce_str); + $test->setName('BadNonceSplit ' . var_export($nonce_str, true)); + $this->addTest($test); + } + + function makeCheckTimestampTests() + { + $cases = array( + // exact, no allowed skew + array('1970-01-01T00:00:00Z', 0, 0, true), + + // exact, large skew + array('1970-01-01T00:00:00Z', 1000, 0, true), + + // no allowed skew, one second old + array('1970-01-01T00:00:00Z', 0, 1, false), + + // many seconds old, outside of skew + array('1970-01-01T00:00:00Z', 10, 50, false), + + // one second old, one second skew allowed + array('1970-01-01T00:00:00Z', 1, 1, true), + + // One second in the future, one second skew allowed + array('1970-01-01T00:00:02Z', 1, 1, true), + + // two seconds in the future, one second skew allowed + array('1970-01-01T00:00:02Z', 1, 0, false), + + // malformed nonce string + array('monkeys', 0, 0, false) + ); + + foreach ($cases as $case) { + $this->_mkCheckTest($case); + } + } + + function _mkCheckTest($case) + { + list($nonce_str, $skew, $now, $expected) = $case; + $test = new Tests_Auth_OpenID_Nonce_TimestampCase( + $nonce_str, $skew, $now, $expected); + $test->setName('CheckTimestamp ' . var_export($nonce_str, true)); + $this->addTest($test); + } +} + +class Tests_Auth_OpenID_Nonce_TimestampCase extends PHPUnit_TestCase { + function Tests_Auth_OpenID_Nonce_TimestampCase( + $nonce_str, $skew, $now, $expected) + { + $this->nonce_string = $nonce_str; + $this->allowed_skew = $skew; + $this->now = $now; + $this->expected = $expected; + } + + function runTest() + { + $actual = Auth_OpenID_checkTimestamp($this->nonce_string, + $this->allowed_skew, + $this->now); + $this->assertEquals($this->expected, $actual); + } +} + +class Tests_Auth_OpenID_NonceTests extends PHPUnit_TestCase { + function test_mkNonce() + { + $nonce_str = Auth_OpenID_mkNonce(); + $this->assertTrue(preg_match(Tests_Auth_OpenID_nonce_re, $nonce_str)); + } + + function test_mkNonce_when() + { + $nonce_str = Auth_OpenID_mkNonce(0); + $this->assertTrue(preg_match(Tests_Auth_OpenID_nonce_re, $nonce_str)); + $tpart = substr($nonce_str, 0, 20); + $this->assertEquals('1970-01-01T00:00:00Z', $tpart); + } + + function test_splitNonce() + { + $s = '1970-01-01T00:00:00Z'; + $expected_t = 0; + $expected_salt = ''; + list($actual_t, $actual_salt) = Auth_OpenID_splitNonce($s); + $this->assertEquals($expected_t, $actual_t); + $this->assertEquals($expected_salt, $actual_salt); + } + + + function test_mkSplit() + { + $t = 42;; + $nonce_str = Auth_OpenID_mkNonce($t); + $this->assertTrue(preg_match(Tests_Auth_OpenID_nonce_re, $nonce_str)); + list($et, $salt) = Auth_OpenID_splitNonce($nonce_str); + $this->assertEquals(6, strlen($salt)); + $this->assertEquals($et, $t); + } +} + +class Tests_Auth_OpenID_Nonce_BadSplitCase extends PHPUnit_TestCase { + function Tests_Auth_OpenID_Nonce_BadSplitCase($nonce_str) + { + $this->nonce_str = $nonce_str; + } + + function runTest() + { + $result = Auth_OpenID_splitNonce($this->nonce_str); + $this->assertNull($result); + } +} + +?> diff --git a/Tests/Auth/OpenID/OpenID_Yadis.php b/Tests/Auth/OpenID/OpenID_Yadis.php new file mode 100644 index 0000000..02981c0 --- /dev/null +++ b/Tests/Auth/OpenID/OpenID_Yadis.php @@ -0,0 +1,230 @@ +<?php + +/** + * Tests for the combination of Yadis discovery and the OpenID + * protocol. + */ + +require_once "PHPUnit.php"; +require_once "Auth/Yadis/XRDS.php"; +require_once "Auth/OpenID/Discover.php"; + +global $__XRDS_BOILERPLATE; +$__XRDS_BOILERPLATE = '<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + xmlns:openid="http://openid.net/xmlns/1.0"> + <XRD> +%s + </XRD> +</xrds:XRDS> +'; + +// Different sets of server URLs for use in the URI tag +global $__server_url_options; +$__server_url_options = array( + array(), // This case should not generate an endpoint object + array('http://server.url/'), + array('https://server.url/'), + array('https://server.url/', 'http://server.url/'), + array('https://server.url/', + 'http://server.url/', + 'http://example.server.url/'), + ); + +// A couple of example extension type URIs. These are not at all +// official, but are just here for testing. +global $__ext_types; +$__ext_types = array( + 'http://janrain.com/extension/blah', + 'http://openid.net/sreg/1.0'); + +// All valid combinations of Type tags that should produce an OpenID +// endpoint +global $__openid_types; +$__openid_types = array( + Auth_OpenID_TYPE_1_0, + Auth_OpenID_TYPE_1_1); + +$temp = array(); +foreach (__subsets($__ext_types) as $exts) { + foreach (__subsets($__openid_types) as $ts) { + if ($ts) { + $temp[] = array_merge($exts, $ts); + } + } +} + +global $__type_uri_options; +$__type_uri_options = $temp; + +// Range of valid Delegate tag values for generating test data +global $__delegate_options; +$__delegate_options = array( + null, + 'http://vanity.domain/', + 'https://somewhere/yadis/'); + +$temp = array(); +foreach ($__delegate_options as $delegate) { + foreach ($__type_uri_options as $type_uris) { + foreach ($__server_url_options as $uris) { + $temp[] = array($uris, $type_uris, $delegate); + } + } +} + +// All combinations of valid URIs, Type URIs and Delegate tags +global $__data; +$__data = $temp; + +function _mkXRDS($services_str) +{ + global $__XRDS_BOILERPLATE; + return sprintf($__XRDS_BOILERPLATE, $services_str); +} + +function _mkService($uris = null, $type_uris = null, + $delegate = null, $dent = ' ') +{ + $chunks = array($dent, "<Service>\n"); + $dent2 = $dent . ' '; + if ($type_uris) { + foreach ($type_uris as $type_uri) { + $chunks = array_merge($chunks, + array($dent2 . '<Type>', + $type_uri, "</Type>\n")); + } + } + + if ($uris) { + foreach ($uris as $uri) { + if (is_array($uri)) { + list($uri, $prio) = $uri; + } else { + $prio = null; + } + $chunks = array_merge($chunks, array($dent2, '<URI')); + if ($prio !== null) { + $chunks = array_merge($chunks, array(' priority="', strval($prio), '"')); + } + $chunks = array_merge($chunks, array('>', $uri, "</URI>\n")); + } + } + + if ($delegate) { + $chunks = array_merge($chunks, + array($dent2, '<openid:Delegate>', + $delegate, "</openid:Delegate>\n")); + } + + $chunks = array_merge($chunks, array($dent, "</Service>\n")); + + return implode("", $chunks); +} + +// Used for generating test data +function __subsets($list) +{ + // Generate all non-empty sublists of a list + $subsets_list = array(array()); + foreach ($list as $elem) { + + $temp = array(); + foreach ($subsets_list as $t) { + $temp[] = array_merge(array($elem), $t); + } + + $subsets_list = array_merge($subsets_list, $temp); + } + + return $subsets_list; +} + +class Tests_Auth_OpenID_Tester extends PHPUnit_TestCase { + function Tests_Auth_OpenID_Tester($uris, $type_uris, $delegate) + { + $this->uris = $uris; + $this->type_uris = $type_uris; + $this->local_id = $delegate; + parent::PHPUnit_TestCase(); + } + + function setUp() + { + $this->yadis_url = 'http://unit.test/'; + + // Create an XRDS document to parse + $services = _mkService($this->uris, + $this->type_uris, + $this->local_id); + $this->xrds = _mkXRDS($services); + } + + function runTest() + { + // Parse into endpoint objects that we will check + $xrds_object = Auth_Yadis_XRDS::parseXRDS($this->xrds); + + $endpoints = array(); + + if ($xrds_object) { + $endpoints = $xrds_object->services(array('filter_MatchesAnyOpenIDType')); + $endpoints = Auth_OpenID_makeOpenIDEndpoints($this->yadis_url, $endpoints); + } + + // make sure there are the same number of endpoints as + // URIs. This assumes that the type_uris contains at least one + // OpenID type. + $this->assertEquals(count($this->uris), count($endpoints), + "URI <-> Endpoint count"); + + // So that we can check equality on the endpoint types + $type_uris = $this->type_uris; + sort($type_uris); + + + $seen_uris = array(); + foreach ($endpoints as $endpoint) { + $seen_uris[] = $endpoint->server_url; + + // All endpoints will have same yadis_url + $this->assertEquals($this->yadis_url, $endpoint->claimed_id); + + // and delegate + $this->assertEquals($this->local_id, $endpoint->local_id); + + // and types + $actual_types = $endpoint->type_uris; + sort($actual_types); + $this->assertEquals($actual_types, $type_uris); + } + + // So that they will compare equal, because we don't care what + // order they are in + sort($seen_uris); + $uris = $this->uris; + sort($uris); + + // Make sure we saw all URIs, and saw each one once + $this->assertEquals($uris, $seen_uris); + } +} + +class Tests_Auth_OpenID_OpenID_Yadis extends PHPUnit_TestSuite { + function Tests_Auth_OpenID_OpenID_Yadis() + { + global $__data; + foreach ($__data as $case) { + $this->addTest(new Tests_Auth_OpenID_Tester($case[0], $case[1], $case[2])); + } + } + + function getName() + { + return 'Tests_Auth_OpenID_OpenID_Yadis'; + } + +} + +?>
\ No newline at end of file diff --git a/Tests/Auth/OpenID/PAPE.php b/Tests/Auth/OpenID/PAPE.php new file mode 100644 index 0000000..d53fb04 --- /dev/null +++ b/Tests/Auth/OpenID/PAPE.php @@ -0,0 +1,246 @@ +<?php + +require_once "PHPUnit.php"; + +require_once "Auth/OpenID/PAPE.php"; +require_once "Auth/OpenID/Message.php"; +require_once "Auth/OpenID/Server.php"; + +class PapeRequestTestCase extends PHPUnit_TestCase { + function setUp() + { + $this->req = new Auth_OpenID_PAPE_Request(); + } + + function test_construct() + { + $this->assertEquals(array(), $this->req->preferred_auth_policies); + $this->assertEquals(null, $this->req->max_auth_age); + $this->assertEquals('pape', $this->req->ns_alias); + + $req2 = new Auth_OpenID_PAPE_Request(array(PAPE_AUTH_MULTI_FACTOR), 1000); + $this->assertEquals(array(PAPE_AUTH_MULTI_FACTOR), $req2->preferred_auth_policies); + $this->assertEquals(1000, $req2->max_auth_age); + } + + function test_add_policy_uri() + { + $this->assertEquals(array(), $this->req->preferred_auth_policies); + $this->req->addPolicyURI(PAPE_AUTH_MULTI_FACTOR); + $this->assertEquals(array(PAPE_AUTH_MULTI_FACTOR), $this->req->preferred_auth_policies); + $this->req->addPolicyURI(PAPE_AUTH_MULTI_FACTOR); + $this->assertEquals(array(PAPE_AUTH_MULTI_FACTOR), $this->req->preferred_auth_policies); + $this->req->addPolicyURI(PAPE_AUTH_PHISHING_RESISTANT); + $this->assertEquals(array(PAPE_AUTH_MULTI_FACTOR, PAPE_AUTH_PHISHING_RESISTANT), + $this->req->preferred_auth_policies); + $this->req->addPolicyURI(PAPE_AUTH_MULTI_FACTOR); + $this->assertEquals(array(PAPE_AUTH_MULTI_FACTOR, PAPE_AUTH_PHISHING_RESISTANT), + $this->req->preferred_auth_policies); + } + + function test_getExtensionArgs() { + $this->assertEquals(array('preferred_auth_policies' => ''), $this->req->getExtensionArgs()); + $this->req->addPolicyURI('http://uri'); + $this->assertEquals(array('preferred_auth_policies' => 'http://uri'), $this->req->getExtensionArgs()); + $this->req->addPolicyURI('http://zig'); + $this->assertEquals(array('preferred_auth_policies' => 'http://uri http://zig'), $this->req->getExtensionArgs()); + $this->req->max_auth_age = 789; + $this->assertEquals(array('preferred_auth_policies' => 'http://uri http://zig', 'max_auth_age' => '789'), $this->req->getExtensionArgs()); + } + + function test_parseExtensionArgs() { + $args = array('preferred_auth_policies' => 'http://foo http://bar', + 'max_auth_age' => '9'); + $this->req->parseExtensionArgs($args); + $this->assertEquals(9, $this->req->max_auth_age); + $this->assertEquals(array('http://foo','http://bar'), $this->req->preferred_auth_policies); + } + + function test_parseExtensionArgs_empty() { + $this->req->parseExtensionArgs(array()); + $this->assertEquals(null, $this->req->max_auth_age); + $this->assertEquals(array(), $this->req->preferred_auth_policies); + } + + function test_fromOpenIDRequest() { + $openid_req_msg = Auth_OpenID_Message::fromOpenIDArgs(array( + 'mode' => 'checkid_setup', + 'ns' => Auth_OpenID_OPENID2_NS, + 'ns.pape' => Auth_OpenID_PAPE_NS_URI, + 'pape.preferred_auth_policies' => implode(' ', array(PAPE_AUTH_MULTI_FACTOR, PAPE_AUTH_PHISHING_RESISTANT)), + 'pape.max_auth_age' => '5476' + )); + $oid_req = new Auth_OpenID_Request(); + $oid_req->message = $openid_req_msg; + $req = Auth_OpenID_PAPE_Request::fromOpenIDRequest($oid_req); + $this->assertEquals(array(PAPE_AUTH_MULTI_FACTOR, PAPE_AUTH_PHISHING_RESISTANT), $req->preferred_auth_policies); + $this->assertEquals(5476, $req->max_auth_age); + } + + function test_fromOpenIDRequest_no_pape() { + $message = new Auth_OpenID_Message(); + $openid_req = new Auth_OpenID_Request(); + $openid_req->message = $message; + $pape_req = Auth_OpenID_PAPE_Request::fromOpenIDRequest($openid_req); + $this->assertTrue($pape_req === null); + } + + function test_preferred_types() { + $this->req->addPolicyURI(PAPE_AUTH_PHISHING_RESISTANT); + $this->req->addPolicyURI(PAPE_AUTH_MULTI_FACTOR); + $pt = $this->req->preferredTypes(array(PAPE_AUTH_MULTI_FACTOR, + PAPE_AUTH_MULTI_FACTOR_PHYSICAL)); + $this->assertEquals(array(PAPE_AUTH_MULTI_FACTOR), $pt); + } +} + +class PAPE_DummySuccessResponse { + function PAPE_DummySuccessResponse($message, $signed_stuff) + { + $this->message = $message; + $this->signed_stuff = $signed_stuff; + } + + function getSignedNS($ns_uri) + { + return $this->signed_stuff; + } +} + +class PapeResponseTestCase extends PHPUnit_TestCase { + function setUp() { + $this->req = new Auth_OpenID_PAPE_Response(); + } + + function test_construct() { + $this->assertEquals(array(), $this->req->auth_policies); + $this->assertEquals(null, $this->req->auth_time); + $this->assertEquals('pape', $this->req->ns_alias); + $this->assertEquals(null, $this->req->nist_auth_level); + + $req2 = new Auth_OpenID_PAPE_Response(array(PAPE_AUTH_MULTI_FACTOR), + '2001-01-01T04:05:23Z', + 3); + $this->assertEquals(array(PAPE_AUTH_MULTI_FACTOR), $req2->auth_policies); + $this->assertEquals('2001-01-01T04:05:23Z', $req2->auth_time); + $this->assertEquals(3, $req2->nist_auth_level); + } + + function test_add_policy_uri() { + $this->assertEquals(array(), $this->req->auth_policies); + $this->req->addPolicyURI(PAPE_AUTH_MULTI_FACTOR); + $this->assertEquals(array(PAPE_AUTH_MULTI_FACTOR), $this->req->auth_policies); + $this->req->addPolicyURI(PAPE_AUTH_MULTI_FACTOR); + $this->assertEquals(array(PAPE_AUTH_MULTI_FACTOR), $this->req->auth_policies); + $this->req->addPolicyURI(PAPE_AUTH_PHISHING_RESISTANT); + $this->assertEquals(array(PAPE_AUTH_MULTI_FACTOR, PAPE_AUTH_PHISHING_RESISTANT), $this->req->auth_policies); + $this->req->addPolicyURI(PAPE_AUTH_MULTI_FACTOR); + $this->assertEquals(array(PAPE_AUTH_MULTI_FACTOR, PAPE_AUTH_PHISHING_RESISTANT), $this->req->auth_policies); + } + + function test_getExtensionArgs() { + $this->assertEquals(array('auth_policies' => 'none'), $this->req->getExtensionArgs()); + $this->req->addPolicyURI('http://uri'); + $this->assertEquals(array('auth_policies' => 'http://uri'), $this->req->getExtensionArgs()); + $this->req->addPolicyURI('http://zig'); + $this->assertEquals(array('auth_policies' => 'http://uri http://zig'), $this->req->getExtensionArgs()); + $this->req->auth_time = '2008-03-02T12:34:56Z'; + $this->assertEquals(array('auth_policies' => 'http://uri http://zig', 'auth_time' => '2008-03-02T12:34:56Z'), $this->req->getExtensionArgs()); + $this->req->nist_auth_level = 3; + $this->assertEquals(array('auth_policies' => 'http://uri http://zig', 'auth_time' => '2008-03-02T12:34:56Z', 'nist_auth_level' => '3'), $this->req->getExtensionArgs()); + } + + function test_getExtensionArgs_error_auth_age() { + $this->req->auth_time = "foo2008-03-02T12:34:56Z"; + $this->assertEquals(false, $this->req->getExtensionArgs()); + $this->req->auth_time = "2008-03-02T12:34:56Zbar"; + $this->assertEquals(false, $this->req->getExtensionArgs()); + } + + function test_getExtensionArgs_error_nist_auth_level() { + $this->req->nist_auth_level = "high as a kite"; + $this->assertEquals(false, $this->req->getExtensionArgs()); + $this->req->nist_auth_level = 5; + $this->assertEquals(false, $this->req->getExtensionArgs()); + $this->req->nist_auth_level = -1; + $this->assertEquals(false, $this->req->getExtensionArgs()); + } + + function test_parseExtensionArgs() { + $args = array('auth_policies' => 'http://foo http://bar', + 'auth_time' => '2008-03-02T12:34:56Z'); + $this->req->parseExtensionArgs($args); + $this->assertEquals('2008-03-02T12:34:56Z', $this->req->auth_time); + $this->assertEquals(array('http://foo','http://bar'), $this->req->auth_policies); + } + + function test_parseExtensionArgs_empty() { + $this->req->parseExtensionArgs(array()); + $this->assertEquals(null, $this->req->auth_time); + $this->assertEquals(array(), $this->req->auth_policies); + } + + function test_parseExtensionArgs_strict_bogus1() { + $args = array('auth_policies' => 'http://foo http://bar', + 'auth_time' => 'yesterday'); + $this->assertEquals(false, $this->req->parseExtensionArgs($args, true)); + } + + function test_parseExtensionArgs_strict_bogus2() { + $args = array('auth_policies' => 'http://foo http://bar', + 'auth_time' => '63', + 'nist_auth_level' => 'some'); + $this->assertEquals(false, $this->req->parseExtensionArgs($args, true)); + } + + function test_parseExtensionArgs_strict_good() { + $args = array('auth_policies' => 'http://foo http://bar', + 'auth_time' => '2008-03-02T12:34:56Z', + 'nist_auth_level' => '0'); + $this->req->parseExtensionArgs($args, true); + $this->assertEquals(array('http://foo','http://bar'), $this->req->auth_policies); + $this->assertEquals('2008-03-02T12:34:56Z', $this->req->auth_time); + $this->assertEquals(0, $this->req->nist_auth_level); + } + + function test_parseExtensionArgs_nostrict_bogus() { + $args = array('auth_policies' => 'http://foo http://bar', + 'auth_time' => 'the other day', + 'nist_auth_level' => 'some'); + $this->req->parseExtensionArgs($args); + $this->assertEquals(array('http://foo','http://bar'), $this->req->auth_policies); + $this->assertEquals(null, $this->req->auth_time); + $this->assertEquals(null, $this->req->nist_auth_level); + } + + function test_fromSuccessResponse() { + $openid_req_msg = Auth_OpenID_Message::fromOpenIDArgs(array( + 'mode' => 'id_res', + 'ns' => Auth_OpenID_OPENID2_NS, + 'ns.pape' => Auth_OpenID_PAPE_NS_URI, + 'auth_policies' => implode(' ', array(PAPE_AUTH_MULTI_FACTOR, PAPE_AUTH_PHISHING_RESISTANT)), + 'auth_time' => '2008-03-02T12:34:56Z' + )); + $signed_stuff = array( + 'auth_policies' => implode(' ', array(PAPE_AUTH_MULTI_FACTOR, PAPE_AUTH_PHISHING_RESISTANT)), + 'auth_time' => '2008-03-02T12:34:56Z' + ); + $oid_req = new PAPE_DummySuccessResponse($openid_req_msg, $signed_stuff); + $req = Auth_OpenID_PAPE_Response::fromSuccessResponse($oid_req); + $this->assertEquals(array(PAPE_AUTH_MULTI_FACTOR, PAPE_AUTH_PHISHING_RESISTANT), $req->auth_policies); + $this->assertEquals('2008-03-02T12:34:56Z', $req->auth_time); + } +} + +class Tests_Auth_OpenID_PAPE extends PHPUnit_TestSuite { + function getName() { + return "Tests_Auth_OpenID_PAPE"; + } + + function Tests_Auth_OpenID_PAPE() { + $this->addTestSuite('PapeRequestTestCase'); + $this->addTestSuite('PapeResponseTestCase'); + } +} + +?> diff --git a/Tests/Auth/OpenID/Parse.php b/Tests/Auth/OpenID/Parse.php new file mode 100644 index 0000000..9a3547c --- /dev/null +++ b/Tests/Auth/OpenID/Parse.php @@ -0,0 +1,186 @@ +<?php + +/** + * Tests for the Consumer parsing functions. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'Tests/Auth/OpenID/TestUtil.php'; +require_once 'Auth/OpenID/Parse.php'; +require_once 'PHPUnit.php'; + +class Tests_Auth_OpenID_Link extends PHPUnit_TestCase { + function Tests_Auth_OpenID_Link($case) + { + list($desc, $markup, $links, $case_text) = $case; + $this->desc = $desc; + $this->markup = $markup; + $this->expected_links = $links; + $this->case_text = $case_text; + $this->parser = new Auth_OpenID_Parse(); + } + + function getName() + { + return $this->desc; + } + + function runTest() + { + $parsed = $this->parser->parseLinkAttrs($this->markup); + $i = 0; + + foreach ($this->expected_links as $expected) { + list($is_optional_link, $expected_link) = $expected; + if ($is_optional_link && + ($i >= count($parsed))) { + continue; + } + + if (count($parsed) <= $i) { + $i++; + continue; + } + + $act_link = $parsed[$i]; + + $increment = true; + foreach ($expected_link as $attr => $data) { + list($is_optional_attr, $value) = $data; + + if ($is_optional_attr) { + $actual_value = null; + if (array_key_exists($attr, $act_link)) { + $actual_value = $act_link[$attr]; + } else { + continue; + } + } else { + $actual_value = $act_link[$attr]; + } + + if ($is_optional_link && + ($value != $actual_value)) { + $increment = false; + break; + } + + $this->assertEquals($value, $actual_value); + } + + if ($increment) { + $i++; + } + } + + $this->assertEquals($i, count($parsed)); + } +} + +class NumTestCases extends PHPUnit_TestCase { + function NumTestCases($test_cases, $num_tests) + { + $this->test_cases = $test_cases; + $this->num_tests = $num_tests; + } + + function runTest() + { + $this->assertEquals(count($this->test_cases), + $this->num_tests); + } +} + +class Tests_Auth_OpenID_Parse extends PHPUnit_TestSuite { + + function getName() + { + return "Tests_Auth_OpenID_Parse"; + } + + function _parseCheck($cond, $where) + { + if (!$cond) { + trigger_error('Parse error in ' . $where, E_USER_ERROR); + } + } + + function parseLink($line) + { + $parts = explode(" ", $line); + $optional = intval($parts[0] == 'Link*:'); + $this->_parseCheck($optional || ($parts[0] == 'Link:'), __FUNCTION__); + + $attrs = array(); + foreach (array_slice($parts, 1) as $attr) { + list($k, $v) = explode("=", $attr, 2); + if ($k[strlen($k) - 1] == '*') { + $attr_optional = 1; + $k = substr($k, 0, strlen($k) - 1); + } else { + $attr_optional = 0; + } + + $attrs[$k] = array($attr_optional, $v); + } + + return array($optional, $attrs); + } + + function parseCase($s) + { + list($header, $markup) = explode("\n\n", $s, 2); + $lines = explode("\n", $header); + $name = array_shift($lines); + $this->_parseCheck(strpos($name, 'Name: ') == 0, __FUNCTION__); + $desc = substr($name, 6); + $parsed = array(); + foreach ($lines as $line) { + $parsed[] = $this->parseLink($line); + } + + return array($desc, $markup, $parsed); + } + + function parseTests($s) + { + $tests = array(); + + $cases = explode("\n\n\n", $s); + $header = array_shift($cases); + list($tests_line, $unused) = explode("\n", $header, 2); + list($k, $v) = explode(": ", $tests_line); + $this->_parseCheck(('Num Tests' == $k), __FUNCTION__); + $num_tests = intval($v); + + foreach (array_slice($cases, 0, count($cases) - 1) as $case) { + list($desc, $markup, $links) = $this->parseCase($case); + $tests[] = array($desc, $markup, $links, $case); + } + + return array($num_tests, $tests); + } + + function Tests_Auth_OpenID_Parse() + { + $test_data = Tests_Auth_OpenID_readdata('linkparse.txt'); + + list($num_tests, $test_cases) = $this->parseTests($test_data); + + $this->addTest(new NumTestCases($test_cases, $num_tests)); + + foreach ($test_cases as $case) { + $this->addTest(new Tests_Auth_OpenID_Link($case)); + } + } +} + +?> diff --git a/Tests/Auth/OpenID/RPVerify.php b/Tests/Auth/OpenID/RPVerify.php new file mode 100644 index 0000000..59c8b1a --- /dev/null +++ b/Tests/Auth/OpenID/RPVerify.php @@ -0,0 +1,299 @@ +<?php + +/* + * Unit tests for verification of return_to URLs for a realm. + */ + +require_once 'Auth/OpenID/Discover.php'; +require_once 'Auth/OpenID/TrustRoot.php'; + +require_once 'Auth/Yadis/Yadis.php'; + +require_once 'PHPUnit.php'; + +// Because "null" cannot be passed by reference. +$NULL_FETCHER = null; + +/* + * Tests for building the discovery URL from a realm and a return_to + * URL + */ +class Tests_Auth_OpenID_BuildDiscoveryURL extends PHPUnit_TestCase { + /* + * Build a discovery URL out of the realm and a return_to and make + * sure that it matches the expected discovery URL + */ + function failUnlessDiscoURL($realm, $expected_discovery_url) + { + $actual_discovery_url = Auth_OpenID_TrustRoot::buildDiscoveryURL($realm); + $this->assertEquals($expected_discovery_url, $actual_discovery_url); + } + + /* + * There is no wildcard and the realm is the same as the return_to + * URL + */ + function test_trivial() + { + $this->failUnlessDiscoURL('http://example.com/foo', + 'http://example.com/foo'); + } + + /* + * There is a wildcard + */ + function test_wildcard() + { + $this->failUnlessDiscoURL('http://*.example.com/foo', + 'http://www.example.com/foo'); + } +} + +class _MockDiscover { + function _MockDiscover(&$data) { + $this->data =& $data; + } + + function mockDiscover($uri, $fetcher, $discover_function=null) + { + $result = new Auth_Yadis_DiscoveryResult($uri); + $result->response_text = $this->data; + $result->normalized_uri = $uri; + return $result; + } +} + +class Tests_Auth_OpenID_ExtractReturnToURLs extends PHPUnit_TestCase { + var $disco_url = 'http://example.com/'; + + function failUnlessXRDSHasReturnURLs($data, $expected_return_urls) + { + $discover_object = new _MockDiscover($data); + $actual_return_urls = Auth_OpenID_getAllowedReturnURLs($this->disco_url, $NULL_FETCHER, array(&$discover_object, 'mockDiscover')); + + $this->assertEquals($expected_return_urls, $actual_return_urls); + } + + function failUnlessDiscoveryFailure($text) + { + $discover_object = new _MockDiscover($text); + $this->assertFalse(Auth_OpenID_getAllowedReturnURLs($this->disco_url, $NULL_FETCHER, array(&$discover_object, 'mockDiscover'))); + } + + function test_empty() + { + $this->failUnlessDiscoveryFailure(''); + } + + function test_badXML() + { + $this->failUnlessDiscoveryFailure('>'); + } + + function test_noEntries() + { + $this->failUnlessXRDSHasReturnURLs('<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + > + <XRD> + </XRD> +</xrds:XRDS> +', array()); + } + + function test_noReturnToEntries() + { + $this->failUnlessXRDSHasReturnURLs('<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + > + <XRD> + <Service priority="10"> + <Type>http://specs.openid.net/auth/2.0/server</Type> + <URI>http://www.myopenid.com/server</URI> + </Service> + </XRD> +</xrds:XRDS> +', array()); + } + + function test_oneEntry() + { + $this->failUnlessXRDSHasReturnURLs('<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + > + <XRD> + <Service> + <Type>http://specs.openid.net/auth/2.0/return_to</Type> + <URI>http://rp.example.com/return</URI> + </Service> + </XRD> +</xrds:XRDS> +', array('http://rp.example.com/return')); + } + + function test_twoEntries() + { + $this->failUnlessXRDSHasReturnURLs('<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + > + <XRD> + <Service priority="0"> + <Type>http://specs.openid.net/auth/2.0/return_to</Type> + <URI>http://rp.example.com/return</URI> + </Service> + <Service priority="1"> + <Type>http://specs.openid.net/auth/2.0/return_to</Type> + <URI>http://other.rp.example.com/return</URI> + </Service> + </XRD> +</xrds:XRDS> +', array('http://rp.example.com/return', + 'http://other.rp.example.com/return')); + } + + function test_twoEntries_withOther() + { + $this->failUnlessXRDSHasReturnURLs('<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + > + <XRD> + <Service priority="0"> + <Type>http://specs.openid.net/auth/2.0/return_to</Type> + <URI>http://rp.example.com/return</URI> + </Service> + <Service priority="1"> + <Type>http://specs.openid.net/auth/2.0/return_to</Type> + <URI>http://other.rp.example.com/return</URI> + </Service> + <Service priority="0"> + <Type>http://example.com/LOLCATS</Type> + <URI>http://example.com/invisible+uri</URI> + </Service> + </XRD> +</xrds:XRDS> +', array('http://rp.example.com/return', + 'http://other.rp.example.com/return')); + } +} + +class Tests_Auth_OpenID_ReturnToMatches extends PHPUnit_TestCase { + function test_noEntries() + { + $this->assertFalse(Auth_OpenID_returnToMatches(array(), 'anything')); + } + + function test_exactMatch() + { + $r = 'http://example.com/return.to'; + $this->assertTrue(Auth_OpenID_returnToMatches(array($r), $r)); + } + + function test_garbageMatch() + { + $r = 'http://example.com/return.to'; + $this->assertTrue(Auth_OpenID_returnToMatches( + array('This is not a URL at all. In fact, it has characters, ' . + 'like "<" that are not allowed in URLs', $r), $r)); + } + + function test_descendant() + { + $r = 'http://example.com/return.to'; + $this->assertTrue(Auth_OpenID_returnToMatches(array($r), + 'http://example.com/return.to/user:joe')); + } + + function test_wildcard() + { + $this->assertFalse(Auth_OpenID_returnToMatches( + array('http://*.example.com/return.to'), + 'http://example.com/return.to')); + } + + function test_noMatch() + { + $r = 'http://example.com/return.to'; + $this->assertFalse(Auth_OpenID_returnToMatches(array($r), + 'http://example.com/xss_exploit')); + } +} + +class Verifier { + function Verifier(&$test_case, $return_to) + { + $this->tc =& $test_case; + $this->return_to = $return_to; + } + + function verify($disco_url) + { + $this->tc->assertEquals('http://www.example.com/', $disco_url); + + if ($this->return_to === false) { + return false; + } else { + return array($this->return_to); + } + } +} + +class Tests_Auth_OpenID_VerifyReturnTo extends PHPUnit_TestCase { + + function test_bogusRealm() + { + $this->assertFalse(Auth_OpenID_verifyReturnTo('', 'http://example.com/', $NULL_FETCHER)); + } + + function test_verifyWithDiscoveryCalled() + { + $realm = 'http://*.example.com/'; + $return_to = 'http://www.example.com/foo'; + + $v = new Verifier($this, $return_to); + + $this->assertTrue(Auth_OpenID_verifyReturnTo($realm, $return_to, $NULL_FETCHER, array(&$v, 'verify'))); + } + + function test_verifyFailWithDiscoveryCalled() + { + $realm = 'http://*.example.com/'; + $return_to = 'http://www.example.com/foo'; + + $v = new Verifier($this, 'http://something-else.invalid/'); + + $this->assertFalse(Auth_OpenID_verifyReturnTo($realm, $return_to, $NULL_FETCHER, array(&$v, 'verify'))); + } + + function test_verifyFailIfDiscoveryRedirects() + { + $realm = 'http://*.example.com/'; + $return_to = 'http://www.example.com/foo'; + + $v = new Verifier($this, false); + + $this->assertFalse(Auth_OpenID_verifyReturnTo($realm, $return_to, $NULL_FETCHER, array(&$v, 'verify'))); + } +} + +class Tests_Auth_OpenID_RPVerify extends PHPUnit_TestSuite { + function getName() + { + return "Tests_Auth_OpenID_RPVerify"; + } + + function Tests_Auth_OpenID_RPVerify() + { + $this->addTestSuite('Tests_Auth_OpenID_VerifyReturnTo'); + $this->addTestSuite('Tests_Auth_OpenID_ReturnToMatches'); + $this->addTestSuite('Tests_Auth_OpenID_ExtractReturnToURLs'); + $this->addTestSuite('Tests_Auth_OpenID_BuildDiscoveryURL'); + } +} + + +?>
\ No newline at end of file diff --git a/Tests/Auth/OpenID/SReg.php b/Tests/Auth/OpenID/SReg.php new file mode 100644 index 0000000..5142b63 --- /dev/null +++ b/Tests/Auth/OpenID/SReg.php @@ -0,0 +1,675 @@ +<?php + +/** + * SReg.php testing code. + */ + +require_once 'Auth/OpenID/SReg.php'; +require_once 'Auth/OpenID/Message.php'; +require_once 'Auth/OpenID/Server.php'; + +require_once 'PHPUnit.php'; + +class SRegURITest extends PHPUnit_TestCase { + function test_is11() + { + $this->assertEquals(Auth_OpenID_SREG_NS_URI_1_1, + Auth_OpenID_SREG_NS_URI); + } +} + +class CheckFieldNameTest extends PHPUnit_TestCase { + function test_goodNamePasses() + { + global $Auth_OpenID_sreg_data_fields; + + foreach ($Auth_OpenID_sreg_data_fields as $field_name => $desc) { + $this->assertTrue(Auth_OpenID_checkFieldName($field_name)); + } + } + + function test_badNameFails() + { + $this->assertfalse(Auth_OpenID_checkFieldName('INVALID')); + } + + function test_badTypeFails() + { + $this->assertfalse(Auth_OpenID_checkFieldName(null)); + } +} + +// For supportsSReg test +class FakeEndpoint { + function FakeEndpoint($supported) + { + $this->supported = $supported; + $this->checked_uris = array(); + } + + function usesExtension($namespace_uri) + { + $this->checked_uris[] = $namespace_uri; + return in_array($namespace_uri, $this->supported); + } +} + +class SupportsSRegTest extends PHPUnit_TestCase { + function test_unsupported() + { + $endpoint = new FakeEndpoint(array()); + $this->assertfalse(Auth_OpenID_supportsSReg($endpoint)); + $this->assertEquals(array(Auth_OpenID_SREG_NS_URI_1_1, + Auth_OpenID_SREG_NS_URI_1_0), + $endpoint->checked_uris); + } + + function test_supported_1_1() + { + $endpoint = new FakeEndpoint(array(Auth_OpenID_SREG_NS_URI_1_1)); + $this->assertTrue(Auth_OpenID_supportsSReg($endpoint)); + $this->assertEquals(array(Auth_OpenID_SREG_NS_URI_1_1), + $endpoint->checked_uris); + } + + function test_supported_1_0() + { + $endpoint = new FakeEndpoint(array(Auth_OpenID_SREG_NS_URI_1_0)); + $this->assertTrue(Auth_OpenID_supportsSReg($endpoint)); + $this->assertEquals(array(Auth_OpenID_SREG_NS_URI_1_1, + Auth_OpenID_SREG_NS_URI_1_0), + $endpoint->checked_uris); + } +} + +class FakeMessage { + function FakeMessage() + { + $this->openid1 = false; + $this->namespaces = new Auth_OpenID_NamespaceMap(); + } + + function isOpenID1() + { + return $this->openid1; + } +} + +class GetNSTest extends PHPUnit_TestCase { + function setUp() + { + $this->msg = new FakeMessage(); + } + + function test_openID2Empty() + { + $ns_uri = Auth_OpenID_SRegBase::_getSRegNS($this->msg); + $this->assertEquals($this->msg->namespaces->getAlias($ns_uri), 'sreg'); + $this->assertEquals(Auth_OpenID_SREG_NS_URI, $ns_uri); + } + + function test_openID1Empty() + { + $this->msg->openid1 = true; + $ns_uri = Auth_OpenID_SRegBase::_getSRegNS($this->msg); + $this->assertEquals($this->msg->namespaces->getAlias($ns_uri), 'sreg'); + $this->assertEquals(Auth_OpenID_SREG_NS_URI, $ns_uri); + } + + function test_openID1Defined_1_0() + { + $this->msg->openid1 = true; + $this->msg->namespaces->add(Auth_OpenID_SREG_NS_URI_1_0); + $ns_uri = Auth_OpenID_SRegBase::_getSRegNS($this->msg); + $this->assertEquals(Auth_OpenID_SREG_NS_URI_1_0, $ns_uri); + } + + function test_openID1Defined_1_0_overrideAlias() + { + foreach (array(true, false) as $openid_version) { + foreach (array(Auth_OpenID_SREG_NS_URI_1_0, + Auth_OpenID_SREG_NS_URI_1_1) as $sreg_version) { + foreach (array('sreg', 'bogus') as $alias) { + $this->setUp(); + + $this->msg->openid1 = $openid_version; + $this->assertTrue($this->msg->namespaces->addAlias($sreg_version, $alias) !== null); + $ns_uri = Auth_OpenID_SRegBase::_getSRegNS($this->msg); + $this->assertEquals($this->msg->namespaces->getAlias($ns_uri), $alias); + $this->assertEquals($sreg_version, $ns_uri); + } + } + } + } + + function test_openID1DefinedBadly() + { + $this->msg->openid1 = true; + $this->msg->namespaces->addAlias('http://invalid/', 'sreg'); + $this->assertTrue(Auth_OpenID_SRegBase::_getSRegNS($this->msg) === null); + } + + function test_openID2DefinedBadly() + { + $this->msg->openid1 = false; + $this->msg->namespaces->addAlias('http://invalid/', 'sreg'); + $this->assertTrue(Auth_OpenID_SRegBase::_getSRegNS($this->msg) === null); + } + + function test_openID2Defined_1_0() + { + $this->msg->namespaces->add(Auth_OpenID_SREG_NS_URI_1_0); + $ns_uri = Auth_OpenID_SRegBase::_getSRegNS($this->msg); + $this->assertEquals(Auth_OpenID_SREG_NS_URI_1_0, $ns_uri); + } + + function test_openID1_sregNSfromArgs() + { + $args = array( + 'sreg.optional' => 'nickname', + 'sreg.required' => 'dob'); + + $m = Auth_OpenID_Message::fromOpenIDArgs($args); + + $this->assertTrue($m->getArg(Auth_OpenID_SREG_NS_URI_1_1, 'optional') == 'nickname'); + $this->assertTrue($m->getArg(Auth_OpenID_SREG_NS_URI_1_1, 'required') == 'dob'); + } +} + +global $__args_sentinel; +global $__ns_sentinel; +$__args_sentinel = 'args_sentinel'; +$__ns_sentinel = 'ns_sentinel'; + +class SentinelFakeMessage { + function SentinelFakeMessage(&$test_case) + { + $this->test_case =& $test_case; + $this->message = new Auth_OpenID_Message(); + } + + function getArgs($ns_uri) + { + global $__ns_sentinel, $__args_sentinel; + $this->test_case->assertEquals($__ns_sentinel, $ns_uri); + return $__args_sentinel; + } +} + +// XXX Ugly hack. Thanks, PHP. +global $__TestingReq_TEST_CASE; +$__TestingReq_TEST_CASE = "FLUB"; + +function __setTestCase(&$thing) { + global $__TestingReq_TEST_CASE; + $__TestingReq_TEST_CASE = $thing; +} + +function &__getTestCase() { + global $__TestingReq_TEST_CASE; + return $__TestingReq_TEST_CASE; +} + +class TestingReq extends Auth_OpenID_SRegRequest { + function fromOpenIDRequest(&$thing, &$test_case) + { + __setTestCase($test_case); + $obj = parent::fromOpenIDRequest($thing, 'TestingReq'); + return $obj; + } + + function _getSRegNS($unused) + { + global $__ns_sentinel; + return $__ns_sentinel; + } + + function parseExtensionArgs($args) + { + global $__args_sentinel; + $tc =& __getTestCase(); + $tc->assertEquals($__args_sentinel, $args); + } +} + +class SRegRequestTest extends PHPUnit_TestCase { + function test_constructEmpty() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertEquals(array(), $req->optional); + $this->assertEquals(array(), $req->required); + $this->assertEquals(null, $req->policy_url); + $this->assertEquals(Auth_OpenID_SREG_NS_URI, $req->ns_uri); + } + + function test_constructFields() + { + $req = Auth_OpenID_SRegRequest::build( + array('nickname'), + array('gender'), + 'http://policy', + 'http://sreg.ns_uri'); + $this->assertEquals(array('gender'), $req->optional); + $this->assertEquals(array('nickname'), $req->required); + $this->assertEquals('http://policy', $req->policy_url); + $this->assertEquals('http://sreg.ns_uri', $req->ns_uri); + } + + function test_constructBadFields() + { + $this->assertTrue(Auth_OpenID_SRegRequest::build(array('elvis')) === null); + } + + function test_fromOpenIDResponse() + { + $openid_req = new Auth_OpenID_Request(); + + $msg = new SentinelFakeMessage($this); + $openid_req->message =& $msg; + + $req = TestingReq::fromOpenIDRequest($openid_req, $this); + $this->assertTrue(is_a($req, 'TestingReq')); + } + + function test_parseExtensionArgs_empty() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertTrue($req->parseExtensionArgs(array())); + } + + function test_parseExtensionArgs_extraIgnored() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertTrue($req->parseExtensionArgs(array('janrain' => 'inc'))); + } + + function test_parseExtensionArgs_nonStrict() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertTrue($req->parseExtensionArgs(array('required' => 'beans'))); + $this->assertEquals(array(), $req->required); + } + + function test_parseExtensionArgs_strict() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertFalse($req->parseExtensionArgs(array('required' => 'beans'), + true)); + } + + function test_parseExtensionArgs_policy() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertTrue($req->parseExtensionArgs( + array('policy_url' => 'http://policy'), true)); + $this->assertEquals('http://policy', $req->policy_url); + } + + function test_parseExtensionArgs_requiredEmpty() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertTrue($req->parseExtensionArgs(array('required' => ''), true)); + $this->assertEquals(array(), $req->required); + } + + function test_parseExtensionArgs_optionalEmpty() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertTrue($req->parseExtensionArgs(array('optional' => ''), true)); + $this->assertEquals(array(), $req->optional); + } + + function test_parseExtensionArgs_optionalSingle() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertTrue($req->parseExtensionArgs(array('optional' => 'nickname'), true)); + $this->assertEquals(array('nickname'), $req->optional); + } + + function test_parseExtensionArgs_optionalList() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertTrue($req->parseExtensionArgs(array('optional' => 'nickname,email'), true)); + $this->assertEquals(array('nickname','email'), $req->optional); + } + + function test_parseExtensionArgs_optionalListBadNonStrict() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertTrue($req->parseExtensionArgs(array('optional' => 'nickname,email,beer'))); + $this->assertEquals(array('nickname','email'), $req->optional); + } + + function test_parseExtensionArgs_optionalListBadStrict() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertFalse($req->parseExtensionArgs(array('optional' => 'nickname,email,beer'), + true)); + } + + function test_parseExtensionArgs_bothNonStrict() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertTrue($req->parseExtensionArgs(array('optional' => 'nickname', + 'required' => 'nickname'))); + $this->assertEquals(array(), $req->optional); + $this->assertEquals(array('nickname'), $req->required); + } + + function test_parseExtensionArgs_bothStrict() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertFalse($req->parseExtensionArgs( + array('optional' => 'nickname', + 'required' => 'nickname'), + true)); + } + + function test_parseExtensionArgs_bothList() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertTrue($req->parseExtensionArgs(array('optional' => 'nickname,email', + 'required' => 'country,postcode'), + true)); + $this->assertEquals(array('nickname','email'), $req->optional); + $this->assertEquals(array('country','postcode'), $req->required); + } + + function test_allRequestedFields() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertEquals(array(), $req->allRequestedFields()); + $req->requestField('nickname'); + $this->assertEquals(array('nickname'), $req->allRequestedFields()); + $req->requestField('gender', true); + $requested = $req->allRequestedFields(); + sort($requested); + $this->assertEquals(array('gender', 'nickname'), $requested); + } + + function test_wereFieldsRequested() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertFalse($req->wereFieldsRequested()); + $req->requestField('gender'); + $this->assertTrue($req->wereFieldsRequested()); + } + + function test_contains() + { + global $Auth_OpenID_sreg_data_fields; + + $req = Auth_OpenID_SRegRequest::build(); + foreach ($Auth_OpenID_sreg_data_fields as $field_name => $desc) { + $this->assertFalse($req->contains($field_name)); + } + + $this->assertFalse($req->contains('something else')); + + $req->requestField('nickname'); + foreach ($Auth_OpenID_sreg_data_fields as $field_name => $desc) { + if ($field_name == 'nickname') { + $this->assertTrue($req->contains($field_name)); + } else { + $this->assertFalse($req->contains($field_name)); + } + } + } + + function test_requestField_bogus() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertFalse($req->requestField('something else')); + $this->assertFalse($req->requestField('something else', true)); + } + + function test_requestField() + { + global $Auth_OpenID_sreg_data_fields; + + // Add all of the fields, one at a time + $req = Auth_OpenID_SRegRequest::build(); + $fields = array_keys($Auth_OpenID_sreg_data_fields); + foreach ($fields as $field_name) { + $req->requestField($field_name); + } + + $this->assertEquals($fields, $req->optional); + $this->assertEquals(array(), $req->required); + + // By default, adding the same fields over again has no effect + foreach ($fields as $field_name) { + $req->requestField($field_name); + } + + $this->assertEquals($fields, $req->optional); + $this->assertEquals(array(), $req->required); + + // Requesting a field as required overrides requesting it as + // optional + $expected = $fields; + $overridden = array_pop($expected); + + $this->assertTrue($req->requestField($overridden, true)); + + $this->assertEquals($expected, $req->optional); + $this->assertEquals(array($overridden), $req->required); + + // Requesting a field as required overrides requesting it as + // optional + foreach ($fields as $field_name) { + $this->assertTrue($req->requestField($field_name, true)); + } + + $this->assertEquals(array(), $req->optional); + foreach ($fields as $f) { + $this->assertTrue(in_array($f, $req->required)); + } + + // Requesting it as optional does not downgrade it to optional + foreach ($fields as $field_name) { + $req->requestField($field_name); + } + + $this->assertEquals(array(), $req->optional); + + foreach ($fields as $f) { + $this->assertTrue(in_array($f, $req->required)); + } + } + + function test_requestFields_type() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertFalse($req->requestFields('nickname')); + } + + function test_requestFields() + { + global $Auth_OpenID_sreg_data_fields; + + // Add all of the fields + $req = Auth_OpenID_SRegRequest::build(); + + $fields = array_keys($Auth_OpenID_sreg_data_fields); + $req->requestFields($fields); + + $this->assertEquals($fields, $req->optional); + $this->assertEquals(array(), $req->required); + + // By default, adding the same fields over again has no effect + $req->requestFields($fields); + + $this->assertEquals($fields, $req->optional); + $this->assertEquals(array(), $req->required); + + // Requesting a field as required overrides requesting it as + // optional + $expected = $fields; + $overridden = array_shift($expected); + $req->requestFields(array($overridden), true); + + foreach ($expected as $f) { + $this->assertTrue(in_array($f, $req->optional)); + } + + $this->assertEquals(array($overridden), $req->required); + + // Requesting a field as required overrides requesting it as + // optional + $req->requestFields($fields, true); + + $this->assertEquals(array(), $req->optional); + $this->assertEquals($fields, $req->required); + + // Requesting it as optional does not downgrade it to optional + $req->requestFields($fields); + + $this->assertEquals(array(), $req->optional); + $this->assertEquals($fields, $req->required); + } + + function test_getExtensionArgs() + { + $req = Auth_OpenID_SRegRequest::build(); + $this->assertEquals(array(), $req->getExtensionArgs()); + + $this->assertTrue($req->requestField('nickname')); + $this->assertEquals(array('optional' => 'nickname'), + $req->getExtensionArgs()); + + $this->assertTrue($req->requestField('email')); + $this->assertEquals(array('optional' => 'nickname,email'), + $req->getExtensionArgs()); + + $this->assertTrue($req->requestField('gender', true)); + $this->assertEquals(array('optional' => 'nickname,email', + 'required' => 'gender'), + $req->getExtensionArgs()); + + $this->assertTrue($req->requestField('postcode', true)); + $this->assertEquals(array('optional' => 'nickname,email', + 'required' => 'gender,postcode'), + $req->getExtensionArgs()); + + $req->policy_url = 'http://policy.invalid/'; + $this->assertEquals(array('optional' => 'nickname,email', + 'required' => 'gender,postcode', + 'policy_url' => 'http://policy.invalid/'), + $req->getExtensionArgs()); + } +} + +class DummySuccessResponse { + function DummySuccessResponse($message, $signed_stuff) + { + $this->message = $message; + $this->signed_stuff = $signed_stuff; + } + + function getSignedNS($ns_uri) + { + return $this->signed_stuff; + } +} + +class SRegResponseTest extends PHPUnit_TestCase { + function test_fromSuccessResponse_signed() + { + $message = Auth_OpenID_Message::fromOpenIDArgs(array( + 'sreg.nickname' => 'The Mad Stork', + )); + $success_resp = new DummySuccessResponse($message, array()); + $sreg_resp = Auth_OpenID_SRegResponse::fromSuccessResponse($success_resp); + $this->assertTrue(count($sreg_resp->contents()) === 0); + } + + function test_fromSuccessResponse_unsigned() + { + $message = Auth_OpenID_Message::fromOpenIDArgs(array( + 'sreg.nickname' => 'The Mad Stork', + )); + + $success_resp = new DummySuccessResponse($message, array()); + $sreg_resp = Auth_OpenID_SRegResponse::fromSuccessResponse($success_resp, + false); + + $this->assertEquals(array('nickname' => 'The Mad Stork'), + $sreg_resp->contents()); + } +} + +class SendFieldsTest extends PHPUnit_TestCase { + function _test($uri) + { + // Create a request message with simple registration fields + $sreg_req = Auth_OpenID_SRegRequest::build(array('nickname', 'email'), + array('fullname')); + $req_msg = new Auth_OpenID_Message($uri); + $req_msg->updateArgs(Auth_OpenID_SREG_NS_URI, + $sreg_req->getExtensionArgs()); + + $req = new Auth_OpenID_Request(); + $req->message =& $req_msg; + $req->namespace = $req_msg->getOpenIDNamespace(); + + // -> send checkid_* request + + // Create an empty response message + $resp_msg = new Auth_OpenID_Message($uri); + $resp = new Auth_OpenID_ServerResponse($req); + $resp->fields = $resp_msg; + + $data = array( + 'nickname' => 'linusaur', + 'postcode' => '12345', + 'country' => 'US', + 'gender' => 'M', + 'fullname' => 'Leonhard Euler', + 'email' => 'president@whitehouse.gov', + 'dob' => '0000-00-00', + 'language' => 'en-us'); + + // Put the requested data fields in the response message + $sreg_resp = Auth_OpenID_SRegResponse::extractResponse($sreg_req, $data); + $resp->addExtension($sreg_resp); + + // <- send id_res response + + // Extract the fields that were sent + $sreg_data_resp = $resp->fields->getArgs(Auth_OpenID_SREG_NS_URI); + $this->assertEquals( + array('nickname' => 'linusaur', + 'email' => 'president@whitehouse.gov', + 'fullname' => 'Leonhard Euler'), + $sreg_data_resp); + } + + function test() + { + foreach (array(Auth_OpenID_OPENID1_NS, + Auth_OpenID_OPENID2_NS) as $uri) { + $this->_test($uri); + } + } +} + +class Tests_Auth_OpenID_SReg extends PHPUnit_TestSuite { + function getName() + { + return "Tests_Auth_OpenID_SReg"; + } + + function Tests_Auth_OpenID_SReg() + { + $this->addTestSuite('SRegURITest'); + $this->addTestSuite('CheckFieldNameTest'); + $this->addTestSuite('SupportsSRegTest'); + $this->addTestSuite('GetNSTest'); + $this->addTestSuite('SRegRequestTest'); + $this->addTestSuite('SRegResponseTest'); + $this->addTestSuite('SendFieldsTest'); + } +} + +?>
\ No newline at end of file diff --git a/Tests/Auth/OpenID/Server.php b/Tests/Auth/OpenID/Server.php new file mode 100644 index 0000000..79a7593 --- /dev/null +++ b/Tests/Auth/OpenID/Server.php @@ -0,0 +1,2464 @@ +<?php + +/** + * Tests for Auth_OpenID_Server + */ + +require_once "PHPUnit.php"; +require_once "Tests/Auth/OpenID/MemStore.php"; +require_once "Auth/OpenID.php"; +require_once "Auth/OpenID/DiffieHellman.php"; +require_once "Auth/OpenID/Server.php"; +require_once "Auth/OpenID/Consumer.php"; + +function altModulus() +{ + $lib =& Auth_OpenID_getMathLib(); + static $num = null; + + if (!$num) { + $num = $lib->init("1423261515703355186607439952816216983770". + "5735494988446894302176757360889904836136". + "0422513557553514790045512299468953431585". + "3008125488594198571710943663581589034331". + "6791551733211386105974742540867014420109". + "9811846875730766487278261498262568348338". + "4764372005569983660877797099908075182915". + "81860338635288400119293970087" + ); + } + + return $num; +} + +global $ALT_GEN; +$ALT_GEN = 5; + +function arrayToString($arr) +{ + $s = "Array("; + + $parts = array(); + foreach ($arr as $k => $v) { + if (is_array($v)) { + $v = arrayToString($v); + } + $parts[] = sprintf("%s => %s", $k, $v); + } + + $s .= implode(", ", $parts); + $s .= ")"; + + return $s; +} + +function _Auth_OpenID_NotAuthorized() +{ + return false; +} + +class Tests_Auth_OpenID_Test_ServerError extends PHPUnit_TestCase { + function test_browserWithReturnTo() + { + $return_to = "http://rp.unittest/consumer"; + // will be a ProtocolError raised by Decode or CheckIDRequest.answer + $args = array( + 'openid.mode' => 'monkeydance', + 'openid.identity' => 'http://wagu.unittest/', + 'openid.return_to' => $return_to); + + $e = new Auth_OpenID_ServerError( + Auth_OpenID_Message::fromPostArgs($args), + "plucky"); + + $this->assertTrue($e->hasReturnTo()); + $expected_args = array( + 'openid.mode' => 'error', + 'openid.error' => 'plucky'); + + $encoded = $e->encodeToURL(); + if (Auth_OpenID_isError($encoded)) { + $this->fail($encoded->toString()); + return; + } + + list($rt_base, $_result_args) = explode("?", $e->encodeToURL(), 2); + $result_args = Auth_OpenID::getQuery($_result_args); + + $this->assertEquals($result_args, $expected_args); + } + + function test_browserWithReturnTo_OpenID2_GET() + { + $return_to = "http://rp.unittest/consumer"; + // will be a ProtocolError raised by Decode or + // CheckIDRequest.answer + $args = Auth_OpenID_Message::fromPostArgs(array( + 'openid.ns' => Auth_OpenID_OPENID2_NS, + 'openid.mode' => 'monkeydance', + 'openid.identity' => 'http://wagu.unittest/', + 'openid.claimed_id' => 'http://wagu.unittest/', + 'openid.return_to' => $return_to)); + + $e = new Auth_OpenID_ServerError($args, "plucky"); + $this->assertTrue($e->hasReturnTo()); + $expected_args = array('openid.ns' => Auth_OpenID_OPENID2_NS, + 'openid.mode' => 'error', + 'openid.error' => 'plucky'); + + list($rt_base, $result_args_s) = explode('?', $e->encodeToURL(), 2); + $result_args = Auth_OpenID::parse_str($result_args_s); + + $this->assertEquals($result_args, $expected_args); + } + + function test_browserWithReturnTo_OpenID2_POST() + { + $return_to = "http://rp.unittest/consumer" . str_repeat('x', Auth_OpenID_OPENID1_URL_LIMIT); + // will be a ProtocolError raised by Decode or + // CheckIDRequest.answer + $args = Auth_OpenID_Message::fromPostArgs(array( + 'openid.ns' => Auth_OpenID_OPENID2_NS, + 'openid.mode' => 'monkeydance', + 'openid.identity' => 'http://wagu.unittest/', + 'openid.claimed_id' => 'http://wagu.unittest/', + 'openid.return_to' => $return_to)); + + $e = new Auth_OpenID_ServerError($args, "plucky"); + $this->assertTrue($e->hasReturnTo()); + $expected_args = array('openid.ns' => Auth_OpenID_OPENID2_NS, + 'openid.mode' => 'error', + 'openid.error' => 'plucky'); + + $this->assertTrue($e->whichEncoding() == Auth_OpenID_ENCODE_HTML_FORM); + + $msg = $e->toMessage(); + + $this->assertTrue($e->toFormMarkup() == + $msg->toFormMarkup($args->getArg(Auth_OpenID_OPENID_NS, 'return_to'))); + } + + function test_browserWithReturnTo_OpenID1_exceeds_limit() + { + $return_to = "http://rp.unittest/consumer" . str_repeat('x', Auth_OpenID_OPENID1_URL_LIMIT); + // will be a ProtocolError raised by Decode or + // CheckIDRequest.answer + $args = Auth_OpenID_Message::fromPostArgs(array( + 'openid.mode' => 'monkeydance', + 'openid.identity' => 'http://wagu.unittest/', + 'openid.return_to' => $return_to)); + + $this->assertTrue($args->isOpenID1()); + + $e = new Auth_OpenID_ServerError($args, "plucky"); + $this->assertTrue($e->hasReturnTo()); + $expected_args = array('openid.mode' => 'error', + 'openid.error' => 'plucky'); + + $this->assertTrue($e->whichEncoding() == Auth_OpenID_ENCODE_URL); + + list($rt_base, $result_args_s) = explode('?', $e->encodeToURL(), 2); + $result_args = Auth_OpenID::parse_str($result_args_s); + $this->assertEquals($result_args, $expected_args); + } + + function test_noReturnTo() + { + // will be a ProtocolError raised by Decode or CheckIDRequest.answer + $args = array( + 'openid.mode' => 'zebradance', + 'openid.identity' => 'http://wagu.unittest/'); + + $e = new Auth_OpenID_ServerError( + Auth_OpenID_Message::fromPostArgs($args), + "waffles"); + + $this->assertFalse($e->hasReturnTo()); + $expected = "error:waffles\nmode:error\n"; + $this->assertEquals($e->encodeToKVForm(), $expected); + } + + function test_noMessage() + { + $e = new Auth_OpenID_ServerError(); + $this->assertFalse($e->hasReturnTo()); + $this->assertEquals($e->whichEncoding(), null); + $this->assertEquals($e->getReturnTo(), null); + } +} + +class Tests_Auth_OpenID_Test_Decode extends PHPUnit_TestCase { + function setUp() + { + $this->id_url = "http://decoder.am.unittest/"; + $this->rt_url = "http://rp.unittest/foobot/?qux=zam"; + $this->tr_url = "http://rp.unittest/"; + $this->assoc_handle = "{assoc}{handle}"; + + $this->claimed_id = 'http://de.legating.de.coder.unittest/'; + $this->op_endpoint = 'http://endpoint.unittest/encode'; + + $this->store = new Tests_Auth_OpenID_MemStore(); + $this->server = new Auth_OpenID_Server($this->store, + $this->op_endpoint); + $this->decoder = new Auth_OpenID_Decoder($this->server); + } + + function test_none() + { + $args = array(); + $r = $this->decoder->decode($args); + $this->assertEquals($r, null); + } + + function test_irrelevant() + { + $args = array( + 'pony' => 'spotted', + 'sreg.mutant_power' => 'decaffinator'); + + $r = $this->decoder->decode($args); + + $this->assertTrue(is_a($r, 'Auth_OpenID_ServerError')); + } + + function test_bad() + { + $args = array( + 'openid.mode' => 'twos-compliment', + 'openid.pants' => 'zippered'); + + // Be sure that decoding the args returns an error. + $result = $this->decoder->decode($args); + + $this->assertTrue(Auth_OpenID_isError($result)); + } + + function test_checkidImmediate() + { + $args = array( + 'openid.mode' => 'checkid_immediate', + 'openid.identity' => $this->id_url, + 'openid.assoc_handle' => $this->assoc_handle, + 'openid.return_to' => $this->rt_url, + 'openid.trust_root' => $this->tr_url, + # should be ignored + 'openid.some.extension' => 'junk'); + + $r = $this->decoder->decode($args); + $this->assertTrue(is_a($r, 'Auth_OpenID_CheckIDRequest')); + $this->assertEquals($r->mode, "checkid_immediate"); + $this->assertEquals($r->immediate, true); + $this->assertEquals($r->identity, $this->id_url); + $this->assertEquals($r->trust_root, $this->tr_url); + $this->assertEquals($r->return_to, $this->rt_url); + $this->assertEquals($r->assoc_handle, $this->assoc_handle); + } + + function test_checkidSetup() + { + $args = array( + 'openid.mode' => 'checkid_setup', + 'openid.identity' => $this->id_url, + 'openid.assoc_handle' => $this->assoc_handle, + 'openid.return_to' => $this->rt_url, + 'openid.trust_root' => $this->tr_url); + + $r = $this->decoder->decode($args); + $this->assertTrue(is_a($r, 'Auth_OpenID_CheckIDRequest')); + $this->assertEquals($r->mode, "checkid_setup"); + $this->assertEquals($r->immediate, false); + $this->assertEquals($r->identity, $this->id_url); + $this->assertEquals($r->trust_root, $this->tr_url); + $this->assertEquals($r->return_to, $this->rt_url); + } + + function test_checkidSetupOpenID2() + { + $args = array( + 'openid.ns' => Auth_OpenID_OPENID2_NS, + 'openid.mode' => 'checkid_setup', + 'openid.identity' => $this->id_url, + 'openid.claimed_id' => $this->claimed_id, + 'openid.assoc_handle' => $this->assoc_handle, + 'openid.return_to' => $this->rt_url, + 'openid.realm' => $this->tr_url + ); + + $r = $this->decoder->decode($args); + $this->assertTrue(is_a($r, 'Auth_OpenID_CheckIDRequest')); + $this->assertEquals($r->mode, "checkid_setup"); + $this->assertEquals($r->immediate, False); + $this->assertEquals($r->identity, $this->id_url); + $this->assertEquals($r->claimed_id, $this->claimed_id); + $this->assertEquals($r->trust_root, $this->tr_url); + $this->assertEquals($r->return_to, $this->rt_url); + } + + function test_checkidSetupNoClaimedIDOpenID2() + { + $args = array( + 'openid.ns' => Auth_OpenID_OPENID2_NS, + 'openid.mode' => 'checkid_setup', + 'openid.identity' => $this->id_url, + 'openid.assoc_handle' => $this->assoc_handle, + 'openid.return_to' => $this->rt_url, + 'openid.realm' => $this->tr_url + ); + + $result = $this->decoder->decode($args); + $this->assertTrue(is_a($result, "Auth_OpenID_ServerError")); + } + + function test_checkidSetupNoIdentityOpenID2() + { + $args = array( + 'openid.ns' => Auth_OpenID_OPENID2_NS, + 'openid.mode' => 'checkid_setup', + 'openid.assoc_handle' => $this->assoc_handle, + 'openid.return_to' => $this->rt_url, + 'openid.realm' => $this->tr_url); + + $r = $this->decoder->decode($args); + $this->assertTrue(is_a($r, 'Auth_OpenID_CheckIDRequest')); + $this->assertEquals($r->mode, "checkid_setup"); + $this->assertEquals($r->immediate, false); + $this->assertEquals($r->identity, null); + $this->assertEquals($r->trust_root, $this->tr_url); + $this->assertEquals($r->return_to, $this->rt_url); + } + + function test_checkidSetupNoReturnOpenID1() + { + $args = array( + 'openid.mode' => 'checkid_setup', + 'openid.identity' => $this->id_url, + 'openid.assoc_handle' => $this->assoc_handle, + 'openid.trust_root' => $this->tr_url); + + $result = $this->decoder->decode($args); + if (!Auth_OpenID_isError($result)) { + $this->fail("Expected Auth_OpenID_ServerError"); + } + } + + function test_checkidSetupNoReturnOpenID2() + { + // Make sure an OpenID 2 request with no return_to can be + // decoded, and make sure a response to such a request raises + // NoReturnToError. + $args = array( + 'openid.ns' => Auth_OpenID_OPENID2_NS, + 'openid.mode' => 'checkid_setup', + 'openid.identity' => $this->id_url, + 'openid.claimed_id' => $this->id_url, + 'openid.assoc_handle' => $this->assoc_handle, + 'openid.realm' => $this->tr_url); + + $req = $this->decoder->decode($args); + + $this->assertTrue(is_a($req, + 'Auth_OpenID_CheckIDRequest')); + + $this->assertTrue(is_a($req->answer(false), 'Auth_OpenID_NoReturnToError')); + $this->assertTrue(is_a($req->encodeToURL('bogus'), 'Auth_OpenID_NoReturnToError')); + $this->assertTrue(is_a($req->getCancelURL(), 'Auth_OpenID_NoReturnToError')); + } + + function test_checkidSetupRealmRequiredOpenID2() + { + // Make sure that an OpenID 2 request which lacks return_to + // cannot be decoded if it lacks a realm. Spec: This value + // (openid.realm) MUST be sent if openid.return_to is omitted. + + $args = array( + 'openid.ns' => Auth_OpenID_OPENID2_NS, + 'openid.mode' => 'checkid_setup', + 'openid.identity' => $this->id_url, + 'openid.assoc_handle' => $this->assoc_handle); + + $this->assertTrue(is_a($this->decoder->decode($args), + 'Auth_OpenID_ServerError')); + } + + function test_checkidSetupBadReturn() + { + $args = array( + 'openid.mode' => 'checkid_setup', + 'openid.identity' => $this->id_url, + 'openid.assoc_handle' => $this->assoc_handle, + 'openid.return_to' => 'not a url'); + + $result = $this->decoder->decode($args);; + if (Auth_OpenID_isError($result)) { + $this->assertTrue($result->message); + } else { + $this->fail(sprintf("Expected ProtocolError, instead " . + "returned with %s", gettype($result))); + } + } + + function test_checkidSetupUntrustedReturn() + { + $args = array( + 'openid.mode' => 'checkid_setup', + 'openid.identity' => $this->id_url, + 'openid.assoc_handle' => $this->assoc_handle, + 'openid.return_to' => $this->rt_url, + 'openid.trust_root' => 'http://not-the-return-place.unittest/'); + + $result = $this->decoder->decode($args); + $this->assertTrue(is_a($result, 'Auth_OpenID_UntrustedReturnURL')); + } + + function test_checkAuth() + { + $args = array( + 'openid.mode' => 'check_authentication', + 'openid.assoc_handle' => '{dumb}{handle}', + 'openid.sig' => 'sigblob', + 'openid.signed' => 'foo,bar,mode', + 'openid.foo' => 'signedval1', + 'openid.bar' => 'signedval2', + 'openid.baz' => 'unsigned'); + + $r = $this->decoder->decode($args); + $this->assertTrue(is_a($r, 'Auth_OpenID_CheckAuthRequest')); + $this->assertEquals($r->mode, 'check_authentication'); + $this->assertEquals($r->sig, 'sigblob'); + } + + function test_checkAuthMissingSignature() + { + $args = array( + 'openid.mode' => 'check_authentication', + 'openid.assoc_handle' => '{dumb}{handle}', + 'openid.signed' => 'foo,bar,mode', + 'openid.foo' => 'signedval1', + 'openid.bar' => 'signedval2', + 'openid.baz' => 'unsigned'); + + $r = $this->decoder->decode($args); + $this->assertTrue(is_a($r, 'Auth_OpenID_ServerError')); + } + + function test_checkAuthAndInvalidate() + { + $args = array( + 'openid.mode' => 'check_authentication', + 'openid.assoc_handle' => '{dumb}{handle}', + 'openid.invalidate_handle' => '[[SMART_handle]]', + 'openid.sig' => 'sigblob', + 'openid.signed' => 'foo,bar,mode', + 'openid.foo' => 'signedval1', + 'openid.bar' => 'signedval2', + 'openid.baz' => 'unsigned'); + + $r = $this->decoder->decode($args); + $this->assertTrue(is_a($r, 'Auth_OpenID_CheckAuthRequest')); + $this->assertEquals($r->invalidate_handle, '[[SMART_handle]]'); + } + + function test_associateDH() + { + if (defined('Auth_OpenID_NO_MATH_SUPPORT')) { + print "(Skipping test_associateDH)"; + return; + } + $args = array( + 'openid.mode' => 'associate', + 'openid.session_type' => 'DH-SHA1', + 'openid.dh_consumer_public' => "Rzup9265tw=="); + + $r = $this->decoder->decode($args); + $this->assertTrue(is_a($r, 'Auth_OpenID_AssociateRequest')); + $this->assertEquals($r->mode, "associate"); + $this->assertEquals($r->session->session_type, "DH-SHA1"); + $this->assertEquals($r->assoc_type, "HMAC-SHA1"); + $this->assertTrue($r->session->consumer_pubkey); + } + + function test_associateDHMissingKey() + { + if (defined('Auth_OpenID_NO_MATH_SUPPORT')) { + print "(Skipping test_associateDHMissingKey)"; + return; + } + $args = array( + 'openid.mode' => 'associate', + 'openid.session_type' => 'DH-SHA1'); + + // Using DH-SHA1 without supplying dh_consumer_public is an error. + $result = $this->decoder->decode($args); + if (!Auth_OpenID_isError($result)) { + $this->fail(sprintf("Expected Auth_OpenID_ServerError, got %s", + gettype($result))); + } + } + + /** + * XXX: Cannot produce a value to break base64_decode + function test_associateDHpubKeyNotB64() + { + $args = array( + 'openid.mode' => 'associate', + 'openid.session_type' => 'DH-SHA1', + 'openid.dh_consumer_public' => "donkeydonkeydonkey"); + + $r = $this->decoder->decode($args); + $this->assertTrue(is_a($r, 'Auth_OpenID_ServerError')); + } + */ + + function test_associateDHModGen() + { + if (defined('Auth_OpenID_NO_MATH_SUPPORT')) { + print "(Skipping test_associateDHModGen)"; + return; + } + + global $ALT_GEN; + + // test dh with non-default but valid values for dh_modulus + // and dh_gen + $lib =& Auth_OpenID_getMathLib(); + + $args = array( + 'openid.mode' => 'associate', + 'openid.session_type' => 'DH-SHA1', + 'openid.dh_consumer_public' => "Rzup9265tw==", + 'openid.dh_modulus' => $lib->longToBase64(altModulus()), + 'openid.dh_gen' => $lib->longToBase64($ALT_GEN)); + + $r = $this->decoder->decode($args); + $this->assertTrue(is_a($r, 'Auth_OpenID_AssociateRequest')); + $this->assertEquals($r->mode, "associate"); + $this->assertEquals($r->session->session_type, "DH-SHA1"); + $this->assertEquals($r->assoc_type, "HMAC-SHA1"); + $this->assertTrue($lib->cmp($r->session->dh->mod, altModulus()) === 0); + $this->assertTrue($lib->cmp($r->session->dh->gen, $ALT_GEN) === 0); + $this->assertTrue($r->session->consumer_pubkey); + } + + /** + * XXX: Can't test invalid base64 values for mod and gen because + * PHP's base64 decoder is much too forgiving or just plain + * broken. + function test_associateDHCorruptModGen() + { + // test dh with non-default but valid values for dh_modulus + // and dh_gen + $args = array( + 'openid.mode' => 'associate', + 'openid.session_type' => 'DH-SHA1', + 'openid.dh_consumer_public' => "Rzup9265tw==", + 'openid.dh_modulus' => 'pizza', + 'openid.dh_gen' => 'gnocchi'); + + $r = $this->decoder->decode($args); + print_r($r); + + $this->assertTrue(is_a($r, 'Auth_OpenID_ServerError')); + } + */ + + function test_associateDHMissingModGen() + { + if (defined('Auth_OpenID_NO_MATH_SUPPORT')) { + print "(Skipping test_associateDHMissingModGen)"; + return; + } + + // test dh with non-default but valid values for dh_modulus + // and dh_gen + $args = array( + 'openid.mode' => 'associate', + 'openid.session_type' => 'DH-SHA1', + 'openid.dh_consumer_public' => "Rzup9265tw==", + 'openid.dh_modulus' => 'pizza'); + + $r = $this->decoder->decode($args); + $this->assertTrue(is_a($r, 'Auth_OpenID_ServerError')); + } + + function test_associateWeirdSession() + { + $args = array( + 'openid.mode' => 'associate', + 'openid.session_type' => 'FLCL6', + 'openid.dh_consumer_public' => "YQ==\n"); + + $r = $this->decoder->decode($args); + $this->assertTrue(is_a($r, 'Auth_OpenID_ServerError')); + } + + function test_associatePlain() + { + $args = array('openid.mode' => 'associate'); + + $r = $this->decoder->decode($args); + $this->assertTrue(is_a($r, 'Auth_OpenID_AssociateRequest')); + $this->assertEquals($r->mode, "associate"); + $this->assertEquals($r->session->session_type, "no-encryption"); + $this->assertEquals($r->assoc_type, "HMAC-SHA1"); + } + + function test_nomode() + { + $args = array( + 'openid.session_type' => 'DH-SHA1', + 'openid.dh_consumer_public' => "my public keeey"); + + $result = $this->decoder->decode($args); + if (!Auth_OpenID_isError($result)) { + $this->fail(sprintf("Expected Auth_OpenID_Error. Got %s", + gettype($result))); + } + } + + function test_invalidns() + { + $args = array('openid.ns' => 'Tuesday', + 'openid.mode' => 'associate'); + + $result = $this->decoder->decode($args); + + $this->assertTrue(is_a($result, 'Auth_OpenID_ServerError')); + + // Assert that the ProtocolError does have a Message attached + // to it, even though the request wasn't a well-formed Message. + $this->assertTrue($result->message); + + // The error message contains the bad openid.ns. + $this->assertTrue(strpos($result->text, 'Tuesday') != -1); + } +} + +class Tests_Auth_OpenID_Test_Encode extends PHPUnit_TestCase { + function setUp() + { + $this->encoder = new Auth_OpenID_Encoder(); + $this->encode = $this->encoder; + $this->op_endpoint = 'http://endpoint.unittest/encode'; + $this->store = new Tests_Auth_OpenID_MemStore(); + $this->server = new Auth_OpenID_Server($this->store, + $this->op_endpoint); + } + + function encode($thing) { + return $this->encoder->encode($thing); + } + + function test_id_res_OpenID2_GET() + { + /* Check that when an OpenID 2 response does not exceed the + OpenID 1 message size, a GET response (i.e., redirect) is + issued. */ + $request = new Auth_OpenID_CheckIDRequest( + 'http://bombom.unittest/', + 'http://burr.unittest/999', + 'http://burr.unittest/', + false, + $this->server->op_endpoint); + + $response = new Auth_OpenID_ServerResponse($request); + $response->fields = Auth_OpenID_Message::fromOpenIDArgs(array( + 'ns' => Auth_OpenID_OPENID2_NS, + 'mode' => 'id_res', + 'identity' => $request->identity, + 'claimed_id' => $request->identity, + 'return_to' => $request->return_to)); + + $this->assertFalse($response->renderAsForm()); + $this->assertTrue($response->whichEncoding() == Auth_OpenID_ENCODE_URL); + $webresponse = $this->encode($response); + $this->assertTrue(array_key_exists('location', $webresponse->headers)); + } + + function test_id_res_OpenID2_POST() + { + /* Check that when an OpenID 2 response exceeds the OpenID 1 + message size, a POST response (i.e., an HTML form) is + returned. */ + $request = new Auth_OpenID_CheckIDRequest( + 'http://bombom.unittest/', + 'http://burr.unittest/999', + 'http://burr.unittest/', + false, + $this->server->op_endpoint); + + $response = new Auth_OpenID_ServerResponse($request); + $response->fields = Auth_OpenID_Message::fromOpenIDArgs(array( + 'ns' => Auth_OpenID_OPENID2_NS, + 'mode' => 'id_res', + 'identity' => $request->identity, + 'claimed_id' => $request->identity, + 'return_to' => str_repeat('x', Auth_OpenID_OPENID1_URL_LIMIT))); + + $this->assertTrue($response->renderAsForm()); + $this->assertTrue(strlen($response->encodeToURL()) > Auth_OpenID_OPENID1_URL_LIMIT); + $this->assertTrue($response->whichEncoding() == Auth_OpenID_ENCODE_HTML_FORM); + $webresponse = $this->encode($response); + $this->assertEquals($webresponse->body, $response->toFormMarkup()); + } + + function test_id_res_OpenID1_exceeds_limit() + { + /* Check that when an OpenID 1 response exceeds the OpenID 1 + message size, a GET response is issued. Technically, this + shouldn't be permitted by the library, but this test is in + place to preserve the status quo for OpenID 1. */ + $request = new Auth_OpenID_CheckIDRequest( + 'http://bombom.unittest/', + 'http://burr.unittest/999', + 'http://burr.unittest/', + false, + $this->server->op_endpoint); + + $response = new Auth_OpenID_ServerResponse($request); + $response->fields = Auth_OpenID_Message::fromOpenIDArgs(array( + 'mode' => 'id_res', + 'identity' => $request->identity, + 'return_to' => str_repeat('x', Auth_OpenID_OPENID1_URL_LIMIT))); + + $this->assertFalse($response->renderAsForm()); + $this->assertTrue(strlen($response->encodeToURL()) > Auth_OpenID_OPENID1_URL_LIMIT); + $this->assertTrue($response->whichEncoding() == Auth_OpenID_ENCODE_URL); + $webresponse = $this->encode($response); + $this->assertEquals($webresponse->headers['location'], $response->encodeToURL()); + } + + function test_id_res() + { + $request = new Auth_OpenID_CheckIDRequest( + 'http://bombom.unittest/', + 'http://burr.unittest/', + 'http://burr.unittest/999', + false, + $this->server); + + $response = new Auth_OpenID_ServerResponse($request); + $response->fields = Auth_OpenID_Message::fromOpenIDArgs( + array( + 'mode' => 'id_res', + 'identity' => $request->identity, + 'return_to' => $request->return_to)); + + $webresponse = $this->encoder->encode($response); + $this->assertEquals($webresponse->code, AUTH_OPENID_HTTP_REDIRECT); + $this->assertTrue(array_key_exists('location', + $webresponse->headers)); + + $location = $webresponse->headers['location']; + $this->assertTrue(strpos($location, $request->return_to) === 0); + // "%s does not start with %s" % ($location, + // $request->return_to)); + + $parsed = parse_url($location); + $query = array(); + $query = Auth_OpenID::parse_str($parsed['query']); + + $expected = $response->fields->toPostArgs(); + $this->assertEquals($query, $expected); + } + + function test_cancel() + { + $request = new Auth_OpenID_CheckIDRequest( + 'http://bombom.unittest/', + 'http://burr.unittest/', + 'http://burr.unittest/999', + false, null, + $this->server); + + $response = new Auth_OpenID_ServerResponse($request); + $response->fields = Auth_OpenID_Message::fromOpenIDArgs(array('mode' => 'cancel')); + + $webresponse = $this->encoder->encode($response); + $this->assertEquals($webresponse->code, AUTH_OPENID_HTTP_REDIRECT); + $this->assertTrue(array_key_exists('location', $webresponse->headers)); + } + + function test_cancelToForm() + { + $request = new Auth_OpenID_CheckIDRequest( + 'http://bombom.unittest/', + 'http://burr.unittest/999', + 'http://burr.unittest/', + false, null, + $this->server); + + $response = new Auth_OpenID_ServerResponse($request); + $response->fields = Auth_OpenID_Message::fromOpenIDArgs(array('mode' => 'cancel')); + + $form = $response->toFormMarkup(); + $pos = strpos($form, 'http://burr.unittest/999'); + $this->assertTrue($pos !== false, var_export($pos, true)); + } + + function test_assocReply() + { + if (!defined('Auth_OpenID_NO_MATH_SUPPORT')) { + $message = new Auth_OpenID_Message(Auth_OpenID_OPENID2_NS); + $message->setArg(Auth_OpenID_OPENID2_NS, 'session_type', + 'no-encryption'); + $request = Auth_OpenID_AssociateRequest::fromMessage($message, + $this->server); + $response = new Auth_OpenID_ServerResponse($request); + $response->fields = Auth_OpenID_Message::fromOpenIDArgs( + array('assoc_handle' => "every-zig")); + $webresponse = $this->encoder->encode($response); + $body = "assoc_handle:every-zig\n"; + $this->assertEquals($webresponse->code, AUTH_OPENID_HTTP_OK); + $this->assertEquals($webresponse->headers, array()); + $this->assertEquals($webresponse->body, $body); + } + } + + function test_checkauthReply() + { + $request = new Auth_OpenID_CheckAuthRequest('a_sock_monkey', + 'siggggg', + array()); + $response = new Auth_OpenID_ServerResponse($request); + $response->fields = Auth_OpenID_Message::fromOpenIDArgs(array( + 'is_valid' => 'true', + 'invalidate_handle' => 'xXxX:xXXx')); + + $body = "invalidate_handle:xXxX:xXXx\nis_valid:true\n"; + $webresponse = $this->encoder->encode($response); + $this->assertEquals($webresponse->code, AUTH_OPENID_HTTP_OK); + $this->assertEquals($webresponse->headers, array()); + $this->assertEquals($webresponse->body, $body); + } + + function test_unencodableError() + { + $args = array('openid.identity' => 'http://limu.unittest/'); + + $e = new Auth_OpenID_ServerError(Auth_OpenID_Message::fromPostArgs($args), + "wet paint"); + + $result = $this->encoder->encode($e); + if (!Auth_OpenID_isError($result, 'Auth_OpenID_EncodingError')) { + $this->fail(sprintf("Expected Auth_OpenID_ServerError, got %s", + gettype($result))); + } + } + + function test_encodableError() + { + $args = array( + 'openid.mode' => 'associate', + 'openid.identity' => 'http://limu.unittest/'); + + $body="error:snoot\nmode:error\n"; + $err = new Auth_OpenID_ServerError(Auth_OpenID_Message::fromPostArgs($args), + "snoot"); + + $webresponse = $this->encoder->encode($err); + $this->assertEquals($webresponse->code, AUTH_OPENID_HTTP_ERROR); + $this->assertEquals($webresponse->headers, array()); + $this->assertEquals($webresponse->body, $body); + } +} + +class Tests_Auth_OpenID_SigningEncode extends PHPUnit_TestCase { + function setUp() + { + // Use filestore here instead of memstore + $this->store = new Tests_Auth_OpenID_MemStore(); + + $this->op_endpoint = 'http://endpoint.unittest/encode'; + + $this->server = new Auth_OpenID_Server($this->store, + $this->op_endpoint); + + $this->request = new Auth_OpenID_CheckIDRequest( + 'http://bombom.unittest/', + 'http://burr.unittest/', + 'http://burr.unittest/999', + false, + null, + $this->server); + + $this->response = new Auth_OpenID_ServerResponse($this->request); + $this->response->fields = Auth_OpenID_Message::fromOpenIDArgs(array( + 'mode' => 'id_res', + 'identity' => $this->request->identity, + 'return_to' => $this->request->return_to)); + + $this->signatory = new Auth_OpenID_Signatory($this->store); + $this->dumb_key = $this->signatory->dumb_key; + $this->normal_key = $this->signatory->normal_key; + + $this->encoder = new Auth_OpenID_SigningEncoder($this->signatory); + } + + function test_idres() + { + $assoc_handle = '{bicycle}{shed}'; + $assoc = Auth_OpenID_Association::fromExpiresIn(60, $assoc_handle, + 'sekrit', 'HMAC-SHA1'); + $this->store->storeAssociation($this->normal_key, $assoc); + $this->request->assoc_handle = $assoc_handle; + $webresponse = $this->encoder->encode($this->response); + $this->assertEquals($webresponse->code, AUTH_OPENID_HTTP_REDIRECT); + $this->assertTrue(array_key_exists('location', + $webresponse->headers)); + + $location = $webresponse->headers['location']; + $parsed = parse_url($location); + $query = Auth_OpenID::getQuery($parsed['query']); + + $this->assertTrue(array_key_exists('openid.sig', $query)); + $this->assertTrue(array_key_exists('openid.assoc_handle', $query)); + $this->assertTrue(array_key_exists('openid.signed', $query)); + } + + function test_idresDumb() + { + $webresponse = $this->encoder->encode($this->response); + $this->assertEquals($webresponse->code, AUTH_OPENID_HTTP_REDIRECT); + $this->assertTrue(array_key_exists('location', $webresponse->headers)); + + $location = $webresponse->headers['location']; + $parsed = parse_url($location); + $query = Auth_OpenID::getQuery($parsed['query']); + + $this->assertTrue(array_key_exists('openid.sig', $query)); + $this->assertTrue(array_key_exists('openid.assoc_handle', $query)); + $this->assertTrue(array_key_exists('openid.signed', $query)); + } + + function test_forgotStore() + { + $this->encoder->signatory = null; + $result = $this->encoder->encode($this->response); + if (!is_a($result, 'Auth_OpenID_ServerError')) { + $this->fail(sprintf("Expected Auth_OpenID_ServerError, got %s", + gettype($result))); + } + } + + function test_cancel() + { + $request = new Auth_OpenID_CheckIDRequest( + 'http://bombom.unittest/', + 'http://burr.unittest/', + 'http://burr.unittest/999', + false, + null, + $this->server); + + $response = new Auth_OpenID_ServerResponse($request, 'cancel'); + $webresponse = $this->encoder->encode($response); + $this->assertEquals($webresponse->code, AUTH_OPENID_HTTP_REDIRECT); + $this->assertTrue(array_key_exists('location', $webresponse->headers)); + $location = $webresponse->headers['location']; + $parsed = parse_url($location); + $query = Auth_OpenID::getQuery($parsed['query']); + + $this->assertFalse(array_key_exists('openid.sig', $query)); + } + + function test_assocReply() + { + if (!defined('Auth_OpenID_NO_MATH_SUPPORT')) { + $message = new Auth_OpenID_Message(Auth_OpenID_OPENID1_NS); + $request = Auth_OpenID_AssociateRequest::fromMessage($message, + $this->server); + $response = new Auth_OpenID_ServerResponse($request); + $response->fields = Auth_OpenID_Message::fromOpenIDArgs( + array('assoc_handle' => "every-zig")); + $webresponse = $this->encoder->encode($response); + $body = "assoc_handle:every-zig\n"; + + $this->assertEquals($webresponse->code, AUTH_OPENID_HTTP_OK); + $this->assertEquals($webresponse->headers, array()); + $this->assertEquals($webresponse->body, $body); + } + } + + function test_alreadySigned() + { + $this->response->fields->setArg(Auth_OpenID_OPENID_NS, 'sig', 'priorSig=='); + $result = $this->encoder->encode($this->response); + if (!is_a($result, 'Auth_OpenID_AlreadySigned')) { + $this->fail(sprintf("Expected Auth_OpenID_AlreadySigned " . + "instance, got %s", gettype($result))); + } + } +} + +class Tests_Auth_OpenID_CheckID extends PHPUnit_TestCase { + function setUp() + { + $this->store = new Tests_Auth_OpenID_MemStore(); + + $this->op_endpoint = 'http://endpoint.unittest/encode'; + + $this->server = new Auth_OpenID_Server($this->store, + $this->op_endpoint); + + $this->request = new Auth_OpenID_CheckIDRequest( + 'http://bambam.unittest/', + 'http://bar.unittest/999', + 'http://bar.unittest/', + false, null, + $this->server); + + $this->request->message = new Auth_OpenID_Message( + Auth_OpenID_OPENID2_NS); + } + + function test_fromMessageClaimedIDWithoutIdentityOpenID2() + { + $name = 'https://example.myopenid.com'; + + $msg = new Auth_OpenID_Message(Auth_OpenID_OPENID2_NS); + $msg->setArg(Auth_OpenID_OPENID_NS, 'mode', 'checkid_setup'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'return_to', + 'http://invalid:8000/rt'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'claimed_id', $name); + + $result = Auth_OpenID_CheckIDRequest::fromMessage( + $msg, $this->server); + + $this->assertTrue(is_a($result, 'Auth_OpenID_ServerError')); + } + + function test_fromMessageIdentityWithoutClaimedIDOpenID2() + { + $name = 'https://example.myopenid.com'; + + $msg = new Auth_OpenID_Message(Auth_OpenID_OPENID2_NS); + $msg->setArg(Auth_OpenID_OPENID_NS, 'mode', 'checkid_setup'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'return_to', + 'http://invalid:8000/rt'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'identity', $name); + + $result = Auth_OpenID_CheckIDRequest::fromMessage( + $msg, $this->server); + + $this->assertTrue(is_a($result, 'Auth_OpenID_ServerError')); + } + + function test_fromMessageWithEmptyTrustRoot() + { + $return_to = 'http://does.not.matter/'; + $msg = Auth_OpenID_Message::fromPostArgs(array( + 'openid.assoc_handle' => '{blah}{blah}{OZivdQ==}', + 'openid.claimed_id' => 'http://delegated.invalid/', + 'openid.identity' => 'http://op-local.example.com/', + 'openid.mode' => 'checkid_setup', + 'openid.ns' => 'http://openid.net/signon/1.0', + 'openid.return_to' => $return_to, + 'openid.trust_root' => '' + )); + $result = Auth_OpenID_CheckIDRequest::fromMessage( + $msg, $this->server); + $this->assertEquals($return_to, $result->trust_root); + } + + function test_trustRootInvalid() + { + $this->request->trust_root = "http://foo.unittest/17"; + $this->request->return_to = "http://foo.unittest/39"; + $this->assertFalse($this->request->trustRootValid()); + } + + function test_trustRootValid() + { + $this->request->trust_root = "http://foo.unittest/"; + $this->request->return_to = "http://foo.unittest/39"; + $this->assertTrue($this->request->trustRootValid()); + } + + function test_malformedTrustRoot() + { + $this->request->trust_root = "invalid://trust*root/"; + $this->request->return_to = "http://foo.unittest/39"; + $sentinel = 'Sentinel'; + $this->request->message = $sentinel; + + $result = $this->request->trustRootValid(); + $this->assertTrue(Auth_OpenID_isError($result)); + $this->assertEquals($result->message, $sentinel); + } + + function _verify($trust_root, $return_to, $value) + { + $this->assertEquals($this->request->trust_root, $trust_root); + $this->assertEquals($this->request->return_to, $return_to); + return $value; + } + + function _verifyTrue($trust_root, $return_to) + { + return $this->_verify($trust_root, $return_to, true); + } + + function _verifyFalse($trust_root, $return_to) + { + return $this->_verify($trust_root, $return_to, false); + } + + /* + * Make sure that verifyReturnTo is calling + * Auth_OpenID_verifyReturnTo + */ + function test_returnToVerified_callsVerify() + { + // Ensure that True and False are passed through unchanged + $this->request->verifyReturnTo = array(&$this, '_verifyTrue'); + $this->assertEquals(true, $this->request->returnToVerified()); + + $this->request->verifyReturnTo = array(&$this, '_verifyFalse'); + $this->assertEquals(false, $this->request->returnToVerified()); + } + + function test_answerToInvalidRoot() + { + $this->request->trust_root = "http://foo.unittest/17"; + $this->request->return_to = "http://foo.unittest/39"; + $result = $this->request->answer(true); + if (!is_a($result, 'Auth_OpenID_UntrustedReturnURL')) { + $this->fail(sprintf("Expected Auth_OpenID_UntrustedReturnURL, " . + "got %s", gettype($result))); + } + $this->assertTrue($this->request->answer(false)); + } + + function _expectAnswer($answer, $identity=null, $claimed_id=null) + { + if (is_a($answer, 'Auth_OpenID_ServerError')) { + $this->fail("Got ServerError, expected valid response in ".$this->getName()); + return; + } + + $expected_list = array( + array('mode', 'id_res'), + array('return_to', $this->request->return_to), + array('op_endpoint', $this->op_endpoint)); + + if ($identity) { + $expected_list[] = array('identity', $identity); + + if ($claimed_id) { + $expected_list[] = array('claimed_id', $claimed_id); + } else { + $expected_list[] = array('claimed_id', $identity); + } + } + + foreach ($expected_list as $pair) { + list($k, $expected) = $pair; + $actual = $answer->fields->getArg(Auth_OpenID_OPENID_NS, $k); + $this->assertEquals($expected, $actual, + "Got wrong value for field '".$k."'"); + } + + $this->assertTrue($answer->fields->hasKey(Auth_OpenID_OPENID_NS, 'response_nonce')); + $this->assertTrue($answer->fields->getOpenIDNamespace() == Auth_OpenID_OPENID2_NS); + + # One for nonce, one for ns + $this->assertEquals(count($answer->fields->toPostArgs()), + count($expected_list) + 2); + } + + function test_answerAllow() + { + $answer = $this->request->answer(true); + + if (Auth_OpenID_isError($answer)) { + $this->fail($answer->toString()); + return; + } + $this->assertEquals($answer->request, $this->request); + $this->_expectAnswer($answer, $this->request->identity); + } + + function test_answerAllowDelegatedIdentity() + { + $this->request->claimed_id = 'http://delegating.unittest/'; + $answer = $this->request->answer(true); + $this->_expectAnswer($answer, $this->request->identity, + $this->request->claimed_id); + } + + function test_answerAllowWithoutIdentityReally() + { + $this->request->identity = null; + $answer = $this->request->answer(true); + $this->assertEquals($answer->request, $this->request); + $this->_expectAnswer($answer); + } + + function test_answerAllowAnonymousFail() + { + $this->request->identity = null; + // XXX - Check on this, I think this behavior is legal in + // OpenID 2.0? + // $this->failUnlessRaises( + // ValueError, $this->request->answer, true, identity="=V"); + $this->assertTrue(is_a($this->request->answer(true, null, "=V"), + 'Auth_OpenID_ServerError')); + } + + function test_answerAllowWithIdentity() + { + $this->request->identity = Auth_OpenID_IDENTIFIER_SELECT; + $selected_id = 'http://anon.unittest/9861'; + $answer = $this->request->answer(true, null, $selected_id); + $this->_expectAnswer($answer, $selected_id); + } + + function test_fromMessageWithoutTrustRoot() + { + $msg = new Auth_OpenID_Message(Auth_OpenID_OPENID2_NS);; + $msg->setArg(Auth_OpenID_OPENID_NS, 'mode', 'checkid_setup'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'return_to', + 'http://real_trust_root/foo'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'assoc_handle', 'bogus'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'identity', 'george'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'claimed_id', 'george'); + + $result = Auth_OpenID_CheckIDRequest::fromMessage( + $msg, $this->server->op_endpoint); + + $this->assertEquals($result->trust_root, + 'http://real_trust_root/foo'); + } + + function test_fromMessageWithoutTrustRootOrReturnTo() + { + $msg = new Auth_OpenID_Message(Auth_OpenID_OPENID2_NS); + $msg->setArg(Auth_OpenID_OPENID_NS, 'mode', 'checkid_setup'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'assoc_handle', 'bogus'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'identity', 'george'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'claimed_id', 'george'); + + $result = Auth_OpenID_CheckIDRequest::fromMessage( + $msg, $this->server); + $this->assertTrue(is_a($result, 'Auth_OpenID_ServerError')); + } + + function test_answerAllowNoEndpointOpenID1() + { + $identity = 'http://bambam.unittest/'; + $reqmessage = Auth_OpenID_Message::fromOpenIDArgs(array( + 'identity' => $identity, + 'trust_root' => 'http://bar.unittest/', + 'return_to' => 'http://bar.unittest/999', + )); + $this->server->op_endpoint = null; + $this->request = Auth_OpenID_CheckIDRequest::fromMessage($reqmessage, $this->server); + $answer = $this->request->answer(true); + + $expected_list = array('mode' => 'id_res', + 'return_to' => $this->request->return_to, + 'identity' => $identity, + ); + + foreach ($expected_list as $k => $expected) { + $actual = $answer->fields->getArg(Auth_OpenID_OPENID_NS, $k); + $this->assertEquals($expected, $actual); + } + + $this->assertTrue($answer->fields->hasKey(Auth_OpenID_OPENID_NS, + 'response_nonce')); + $this->assertTrue($answer->fields->getOpenIDNamespace(), + Auth_OpenID_OPENID1_NS); + $this->assertTrue( + $answer->fields->namespaces->isImplicit(Auth_OpenID_OPENID1_NS)); + + // One for nonce (OpenID v1 namespace is implicit) + $this->assertEquals(count($answer->fields->toPostArgs()), + count($expected_list) + 1, + var_export($answer->fields->toPostArgs(), true)); + } + + function test_answerAllowWithDelegatedIdentityOpenID2() + { + // Answer an IDENTIFIER_SELECT case with a delegated + // identifier. claimed_id delegates to selected_id here. + $this->request->identity = Auth_OpenID_IDENTIFIER_SELECT; + $selected_id = 'http://anon.unittest/9861'; + $claimed_id = 'http://monkeyhat.unittest/'; + $answer = $this->request->answer(true, null, $selected_id, + $claimed_id); + $this->_expectAnswer($answer, $selected_id, $claimed_id); + } + + function test_answerAllowWithDelegatedIdentityOpenID1() + { + // claimed_id parameter doesn't exist in OpenID 1. + $msg = new Auth_OpenID_Message(Auth_OpenID_OPENID1_NS); + $this->request->message = $msg; + // claimed_id delegates to selected_id here. + $this->request->identity = Auth_OpenID_IDENTIFIER_SELECT; + $selected_id = 'http://anon.unittest/9861'; + $claimed_id = 'http://monkeyhat.unittest/'; + + $result = $this->request->answer(true, + null, + $selected_id, + $claimed_id); + + $this->assertTrue(is_a($result, "Auth_OpenID_ServerError"), + var_export($result, true)); + } + + function test_answerAllowWithAnotherIdentity() + { + // XXX - Check on this, I think this behavior is legal is + // OpenID 2.0? + // $this->failUnlessRaises(ValueError, $this->request->answer, true, + // identity="http://pebbles.unittest/"); + $result = $this->request->answer(true, null, "http://pebbles.unittest/"); + $this->assertTrue(is_a($result, "Auth_OpenID_ServerError")); + } + + function test_answerAllowNoIdentityOpenID1() + { + $msg = new Auth_OpenID_Message(Auth_OpenID_OPENID1_NS); + $this->request->message = $msg; + $this->request->identity = null; + // $this->failUnlessRaises(ValueError, $this->request->answer, true, + // identity=null); + $result = $this->request->answer(true); + $this->assertTrue(is_a($result, "Auth_OpenID_ServerError")); + } + + function test_answerAllowForgotEndpoint() + { + $this->request->server->op_endpoint = null; + $result = $this->request->answer(true); + $this->assertTrue(is_a($result, "Auth_OpenID_ServerError")); + } + + function test_checkIDWithNoIdentityOpenID1() + { + $msg = new Auth_OpenID_Message(Auth_OpenID_OPENID1_NS); + $msg->setArg(Auth_OpenID_OPENID_NS, 'return_to', 'bogus'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'trust_root', 'bogus'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'mode', 'checkid_setup'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'assoc_handle', 'bogus'); + + // $this->failUnlessRaises(server->ProtocolError, + // server->CheckIDRequest->fromMessage, + // msg, $this->server); + $result = Auth_OpenID_CheckIDRequest::fromMessage($msg, $this->server); + + $this->assertTrue(is_a($result, 'Auth_OpenID_ServerError')); + } + + function test_trustRootOpenID1() + { + // Ignore openid.realm in OpenID 1 + $msg = new Auth_OpenID_Message(Auth_OpenID_OPENID1_NS); + $msg->setArg(Auth_OpenID_OPENID_NS, 'mode', 'checkid_setup'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'trust_root', 'http://real_trust_root/'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'realm', 'http://fake_trust_root/'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'return_to', 'http://real_trust_root/foo'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'assoc_handle', 'bogus'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'identity', 'george'); + + $result = Auth_OpenID_CheckIDRequest::fromMessage($msg, $this->server); + + $this->assertTrue($result->trust_root == 'http://real_trust_root/'); + } + + function test_trustRootOpenID2() + { + // Ignore openid.trust_root in OpenID 2 + $msg = new Auth_OpenID_Message(Auth_OpenID_OPENID2_NS); + $msg->setArg(Auth_OpenID_OPENID_NS, 'mode', 'checkid_setup'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'realm', 'http://real_trust_root/'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'trust_root', 'http://fake_trust_root/'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'return_to', 'http://real_trust_root/foo'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'assoc_handle', 'bogus'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'identity', 'george'); + $msg->setArg(Auth_OpenID_OPENID_NS, 'claimed_id', 'george'); + + $result = Auth_OpenID_CheckIDRequest::fromMessage($msg, $this->server); + + $this->assertTrue($result->trust_root == 'http://real_trust_root/'); + } + + function test_encodeToURL() + { + $server_url = 'http://openid-server.unittest/'; + $result = $this->request->encodeToURL($server_url); + + $this->assertFalse(is_a($result, 'Auth_OpenID_ServerError')); + + // How to check? How about a round-trip test. + list($base, $result_args) = explode("?", $result, 2); + $args = Auth_OpenID::getQuery($result_args); + $message = Auth_OpenID_Message::fromPostArgs($args); + + $rebuilt_request = Auth_OpenID_CheckIDRequest::fromMessage($message, + $this->server); + // argh, lousy hack + $this->assertTrue($rebuilt_request->equals($this->request)); + } + + function test_answerAllowNoTrustRoot() + { + $this->request->trust_root = null; + $answer = $this->request->answer(true); + $this->assertEquals($answer->request, $this->request); + $this->_expectAnswer($answer, $this->request->identity); + } + + function test_answerImmediateDenyOpenID1() + { + $msg = new Auth_OpenID_Message(Auth_OpenID_OPENID1_NS); + $this->request->message = $msg; + $this->request->namespace = $msg->getOpenIDNamespace(); + $this->request->mode = 'checkid_immediate'; + $this->request->claimed_id = 'http://claimed-id.test/'; + $this->request->immediate = true; + $server_url = "http://setup-url.unittest/"; + $answer = $this->request->answer(false, $server_url); + + $this->assertEquals($answer->request, $this->request); + $this->assertEquals(count($answer->fields->toPostArgs()), 2); + $this->assertEquals($answer->fields->getOpenIDNamespace(), + Auth_OpenID_OPENID1_NS); + $this->assertTrue( + $answer->fields->namespaces->isImplicit(Auth_OpenID_OPENID1_NS)); + $this->assertEquals($answer->fields->getArg(Auth_OpenID_OPENID_NS, 'mode'), + 'id_res'); + + $usu = $answer->fields->getArg(Auth_OpenID_OPENID_NS,'user_setup_url'); + $this->assertTrue(strpos($usu, $server_url) == 0); + $expected_substr = 'openid.claimed_id=http%3A%2F%2Fclaimed-id.test%2F'; + $this->assertTrue(strpos($usu, $expected_substr), $usu); + } + + function test_answerImmediateDenyOpenID2() + { + $this->request->mode = 'checkid_immediate'; + $this->request->immediate = true; + $server_url = "http://setup-url.unittest/"; + $answer = $this->request->answer(false, $server_url); + + $this->assertEquals($answer->request, $this->request); + $this->assertEquals(count($answer->fields->toPostArgs()), 3); + $this->assertEquals($answer->fields->getOpenIDNamespace(), + Auth_OpenID_OPENID2_NS); + $this->assertEquals($answer->fields->getArg(Auth_OpenID_OPENID_NS, 'mode'), + 'setup_needed'); + } + + function test_answerSetupDeny() + { + $answer = $this->request->answer(false); + $this->assertEquals($answer->fields->getArgs(Auth_OpenID_OPENID_NS), + array('mode' => 'cancel')); + } + + function test_getCancelURL() + { + $url = $this->request->getCancelURL(); + + $parsed = parse_url($url); + $query = Auth_OpenID::getQuery($parsed['query']); + + $this->assertEquals(array('openid.mode' => 'cancel', + 'openid.ns' => Auth_OpenID_OPENID2_NS), + $query); + } + + function test_getCancelURLimmed() + { + $this->request->mode = 'checkid_immediate'; + $this->request->immediate = true; + $result = $this->request->getCancelURL(); + if (!is_a($result, 'Auth_OpenID_ServerError')) { + $this->fail(sprintf("Expected Auth_OpenID_ServerError, got %s", + gettype($result))); + } + } +} + +class Tests_Auth_OpenID_CheckIDExtension extends PHPUnit_TestCase { + function setUp() + { + $this->op_endpoint = 'http://endpoint.unittest/ext'; + $this->store = new Tests_Auth_OpenID_MemStore(); + $this->server = new Auth_OpenID_Server($this->store, $this->op_endpoint); + $this->request = new Auth_OpenID_CheckIDRequest( + 'http://bambam.unittest/', + 'http://bar.unittest/', + 'http://bar.unittest/999', + false, + null, + $this->server); + + $this->response = new Auth_OpenID_ServerResponse($this->request); + $this->response->fields->setArg(Auth_OpenID_OPENID_NS, 'mode', 'id_res'); + $this->response->fields->setArg(Auth_OpenID_OPENID_NS, 'blue', 'star'); + } + + function test_addField() + { + $namespace = 'something:'; + $this->response->fields->setArg($namespace, 'bright', 'potato'); + $this->assertEquals($this->response->fields->getArgs(Auth_OpenID_OPENID_NS), + array('blue' => 'star', + 'mode' => 'id_res')); + + $this->assertEquals($this->response->fields->getArgs($namespace), + array('bright' => 'potato')); + } + + function test_addFields() + { + $namespace = 'mi5:'; + $args = array('tangy' => 'suspenders', + 'bravo' => 'inclusion'); + + $this->response->fields->updateArgs($namespace, $args); + $this->assertEquals($this->response->fields->getArgs(Auth_OpenID_OPENID_NS), + array('blue' => 'star', + 'mode' => 'id_res')); + $this->assertEquals($this->response->fields->getArgs($namespace), $args); + } +} + +class _MockSignatory { + var $isValid = true; + + function _MockSignatory($assoc) + { + $this->assocs = array($assoc); + } + + function verify($assoc_handle, $message) + { + if (!$message->hasKey(Auth_OpenID_OPENID_NS, 'sig')) { + return false; + } + + if (in_array(array(true, $assoc_handle), $this->assocs)) { + return $this->isValid; + } else { + return false; + } + } + + function getAssociation($assoc_handle, $dumb) + { + if (in_array(array($dumb, $assoc_handle), $this->assocs)) { + // This isn't a valid implementation for many uses of this + // function, mind you. + return true; + } else { + return null; + } + } + + function invalidate($assoc_handle, $dumb) + { + if (in_array(array($dumb, $assoc_handle), $this->assocs)) { + $i = 0; + foreach ($this->assocs as $pair) { + if ($pair == array($dumb, $assoc_handle)) { + unset($this->assocs[$i]); + break; + } + $i++; + } + } + } +} + +class Tests_Auth_OpenID_CheckAuth extends PHPUnit_TestCase { + function setUp() + { + $this->assoc_handle = 'mooooooooo'; + $this->message = Auth_OpenID_Message::fromPostArgs( + array('openid.sig' => 'signarture', + 'one' => 'alpha', + 'two' => 'beta')); + + $this->request = new Auth_OpenID_CheckAuthRequest( + $this->assoc_handle, $this->message); + + $this->signatory = new _MockSignatory(array(true, $this->assoc_handle)); + } + + function test_valid() + { + $this->request->namespace = Auth_OpenID_OPENID1_NS; + $r = $this->request->answer($this->signatory); + $this->assertEquals($r->fields->getArgs(Auth_OpenID_OPENID1_NS), + array('is_valid' => 'true')); + $this->assertEquals($r->request, $this->request); + } + + function test_invalid() + { + $this->request->namespace = Auth_OpenID_OPENID1_NS; + $this->signatory->isValid = false; + $r = $this->request->answer($this->signatory); + $this->assertEquals($r->fields->getArgs(Auth_OpenID_OPENID1_NS), + array('is_valid' => 'false')); + } + + function test_replay() + { + $this->request->namespace = Auth_OpenID_OPENID1_NS; + $r = $this->request->answer($this->signatory); + $r = $this->request->answer($this->signatory); + $this->assertEquals($r->fields->getArgs(Auth_OpenID_OPENID1_NS), + array('is_valid' => 'false')); + } + + function test_invalidatehandle() + { + $this->request->namespace = Auth_OpenID_OPENID1_NS; + $this->request->invalidate_handle = "bogusHandle"; + $r = $this->request->answer($this->signatory); + $this->assertEquals($r->fields->getArgs(Auth_OpenID_OPENID1_NS), + array('is_valid' => 'true', + 'invalidate_handle' => "bogusHandle")); + $this->assertEquals($r->request, $this->request); + } + + function test_invalidatehandleNo() + { + $this->request->namespace = Auth_OpenID_OPENID1_NS; + $assoc_handle = 'goodhandle'; + $this->signatory->assocs[] = array(false, 'goodhandle'); + $this->request->invalidate_handle = $assoc_handle; + $r = $this->request->answer($this->signatory); + $this->assertEquals($r->fields->getArgs(Auth_OpenID_OPENID1_NS), + array('is_valid' => 'true')); + } +} + +class Tests_Auth_OpenID_Associate extends PHPUnit_TestCase { + // TODO: test DH with non-default values for modulus and gen. + // (important to do because we actually had it broken for a + // while.) + + function setUp() + { + $message = new Auth_OpenID_Message(Auth_OpenID_OPENID1_NS); + $this->request = Auth_OpenID_AssociateRequest::fromMessage($message); + $this->store = new Tests_Auth_OpenID_MemStore(); + $this->signatory = new Auth_OpenID_Signatory($this->store); + } + + function test_dhSHA1() + { + if (!defined('Auth_OpenID_NO_MATH_SUPPORT')) { + $this->assoc = $this->signatory->createAssociation(false, + 'HMAC-SHA1'); + + $dh = new Auth_OpenID_DiffieHellman(); + $ml =& Auth_OpenID_getMathLib(); + + $cpub = $dh->public; + $session = new Auth_OpenID_DiffieHellmanSHA1ServerSession( + new Auth_OpenID_DiffieHellman(), + $cpub); + + $this->request = new Auth_OpenID_AssociateRequest($session, + 'HMAC-SHA1'); + $response = $this->request->answer($this->assoc); + + $this->assertEquals( + $response->fields->getArg(Auth_OpenID_OPENID_NS, "assoc_type"), + "HMAC-SHA1"); + + $this->assertEquals( + $response->fields->getArg(Auth_OpenID_OPENID_NS, "assoc_handle"), + $this->assoc->handle); + + $this->assertFalse( + $response->fields->getArg(Auth_OpenID_OPENID_NS, "mac_key")); + + $this->assertEquals( + $response->fields->getArg(Auth_OpenID_OPENID_NS, "session_type"), + "DH-SHA1"); + + $this->assertTrue( + $response->fields->getArg(Auth_OpenID_OPENID_NS, "enc_mac_key")); + + $this->assertTrue( + $response->fields->getArg(Auth_OpenID_OPENID_NS, + "dh_server_public")); + + $enc_key = base64_decode( + $response->fields->getArg(Auth_OpenID_OPENID_NS, "enc_mac_key")); + + $spub = $ml->base64ToLong( + $response->fields->getArg(Auth_OpenID_OPENID_NS, + "dh_server_public")); + + $secret = $dh->xorSecret($spub, $enc_key, $session->hash_func); + + $this->assertEquals($secret, $this->assoc->secret); + } + } + + function test_dhSHA256() + { + if (defined('Auth_OpenID_NO_MATH_SUPPORT') || + !Auth_OpenID_SHA256_SUPPORTED) { + print "(Skipping test_dhSHA256)"; + return; + } + + $this->assoc = $this->signatory->createAssociation(false, + 'HMAC-SHA256'); + $consumer_dh = new Auth_OpenID_DiffieHellman(); + $cpub = $consumer_dh->public; + $server_dh = new Auth_OpenID_DiffieHellman(); + $session = new Auth_OpenID_DiffieHellmanSHA256ServerSession($server_dh, $cpub); + + $this->request = new Auth_OpenID_AssociateRequest($session, 'HMAC-SHA256'); + $response = $this->request->answer($this->assoc); + + $this->assertFalse($response->fields->getArg(Auth_OpenID_OPENID_NS, "mac_key")); + $this->assertTrue($response->fields->getArg(Auth_OpenID_OPENID_NS, "enc_mac_key")); + $this->assertTrue($response->fields->getArg(Auth_OpenID_OPENID_NS, "dh_server_public")); + + $fields = array( + 'assoc_type' => 'HMAC-SHA256', + 'assoc_handle' => $this->assoc->handle, + 'session_type' => 'DH-SHA256', + ); + + foreach ($fields as $k => $v) { + $this->assertEquals( + $response->fields->getArg(Auth_OpenID_OPENID_NS, $k), $v); + } + + $enc_key = base64_decode( + $response->fields->getArg(Auth_OpenID_OPENID_NS, "enc_mac_key")); + + $lib =& Auth_OpenID_getMathLib(); + $spub = $lib->base64ToLong($response->fields->getArg(Auth_OpenID_OPENID_NS, + "dh_server_public")); + $secret = $consumer_dh->xorSecret($spub, $enc_key, 'Auth_OpenID_SHA256'); + + $s = base64_encode($secret); + $assoc_s = base64_encode($this->assoc->secret); + + $this->assertEquals($s, $assoc_s); + } + + function test_protoError256() + { + if (defined('Auth_OpenID_NO_MATH_SUPPORT') || + !Auth_OpenID_HMACSHA256_SUPPORTED) { + print "(Skipping test_protoError256)"; + return; + } + + $s256_session = new Auth_OpenID_DiffieHellmanSHA256ConsumerSession(); + + $invalid_s256 = array('openid.assoc_type' => 'HMAC-SHA1', + 'openid.session_type' => 'DH-SHA256'); + + $invalid_s256 = array_merge($invalid_s256, $s256_session->getRequest()); + + $invalid_s256_2 = array('openid.assoc_type' => 'MONKEY-PIRATE', + 'openid.session_type' => 'DH-SHA256'); + + $invalid_s256_2 = array_merge($invalid_s256_2, $s256_session->getRequest()); + + $bad_request_argss = array( + $invalid_s256, + $invalid_s256_2); + + foreach ($bad_request_argss as $request_args) { + $message = Auth_OpenID_Message::fromPostArgs($request_args); + $result = Auth_OpenID_Associaterequest::fromMessage($message); + $this->assertTrue(is_a($result, 'Auth_OpenID_ServerError')); + } + } + + function test_plaintext() + { + $this->assoc = $this->signatory->createAssociation(false, + 'HMAC-SHA1'); + $response = $this->request->answer($this->assoc); + + $this->assertEquals( + $response->fields->getArg(Auth_OpenID_OPENID_NS, "assoc_type"), + "HMAC-SHA1"); + + $this->assertEquals( + $response->fields->getArg(Auth_OpenID_OPENID_NS, "assoc_handle"), + $this->assoc->handle); + + $this->assertEquals( + $response->fields->getArg(Auth_OpenID_OPENID_NS, "expires_in"), + sprintf("%d", $this->signatory->SECRET_LIFETIME)); + + $this->assertEquals( + $response->fields->getArg(Auth_OpenID_OPENID_NS, "mac_key"), + base64_encode($this->assoc->secret)); + + $this->assertFalse($response->fields->getArg(Auth_OpenID_OPENID_NS, + "session_type")); + + $this->assertFalse($response->fields->getArg(Auth_OpenID_OPENID_NS, + "enc_mac_key")); + + $this->assertFalse($response->fields->getArg(Auth_OpenID_OPENID_NS, + "dh_server_public")); + } + + function test_plaintextV2() + { + // The main difference between this and the v1 test is that + // the session_typ is always returned in v2. + $args = array('openid.mode' => 'associate', + 'openid.ns' => Auth_OpenID_OPENID2_NS, + 'openid.assoc_type' => 'HMAC-SHA1', + 'openid.session_type' => 'no-encryption'); + + $this->request = Auth_OpenID_AssociateRequest::fromMessage( + Auth_OpenID_Message::fromPostArgs($args)); + $this->assertFalse($this->request->message->isOpenID1()); + + $this->assoc = $this->signatory->createAssociation(false, + 'HMAC-SHA1'); + $response = $this->request->answer($this->assoc); + + $this->assertEquals( + $response->fields->getArg(Auth_OpenID_OPENID_NS, "assoc_type"), + "HMAC-SHA1"); + + $this->assertEquals( + $response->fields->getArg(Auth_OpenID_OPENID_NS, "assoc_handle"), + $this->assoc->handle); + + $this->assertEquals( + $response->fields->getArg(Auth_OpenID_OPENID_NS, "expires_in"), + sprintf("%d", $this->signatory->SECRET_LIFETIME)); + + $this->assertEquals( + $response->fields->getArg(Auth_OpenID_OPENID_NS, "mac_key"), + base64_encode($this->assoc->secret)); + + $session_type = $response->fields->getArg(Auth_OpenID_OPENID_NS, + "session_type"); + $this->assertEquals('no-encryption', $session_type); + + $this->assertFalse($response->fields->getArg(Auth_OpenID_OPENID_NS, + "enc_mac_key")); + $this->assertFalse($response->fields->getArg(Auth_OpenID_OPENID_NS, + "dh_server_public")); + } + + function test_protoError() + { + $s1_session = new Auth_OpenID_DiffieHellmanSHA1ConsumerSession(); + + $invalid_s1 = array('openid.assoc_type' => 'HMAC-SHA256', + 'openid.session_type' => 'DH-SHA1'); + $invalid_s1 = array_merge($invalid_s1, $s1_session->getRequest()); + + $invalid_s1_2 = array('openid.assoc_type' => 'ROBOT-NINJA', + 'openid.session_type' => 'DH-SHA1'); + $invalid_s1_2 = array_merge($invalid_s1_2, $s1_session->getRequest()); + + $bad_request_argss = array(array('openid.assoc_type' => 'Wha?'), + $invalid_s1, + $invalid_s1_2); + + foreach ($bad_request_argss as $request_args) { + $message = Auth_OpenID_Message::fromPostArgs($request_args); + $result = Auth_OpenID_AssociateRequest::fromMessage($message); + $this->assertTrue(is_a($result, 'Auth_OpenID_ServerError')); + } + } + + function test_protoErrorFields() + { + $contact = 'user@example.invalid'; + $reference = 'Trac ticket number MAX_INT'; + $error = 'poltergeist'; + + $openid1_args = array( + 'openid.identitiy' => 'invalid', + 'openid.mode' => 'checkid_setup'); + + $openid2_args = $openid1_args; + $openid2_args = array_merge($openid2_args, + array('openid.ns' => Auth_OpenID_OPENID2_NS)); + + // Check presence of optional fields in both protocol versions + + $openid1_msg = Auth_OpenID_Message::fromPostArgs($openid1_args); + $p = new Auth_OpenID_ServerError($openid1_msg, $error, + $reference, $contact); + $reply = $p->toMessage(); + + $this->assertEquals($reply->getArg(Auth_OpenID_OPENID_NS, 'reference'), + $reference); + $this->assertEquals($reply->getArg(Auth_OpenID_OPENID_NS, 'contact'), + $contact); + + $openid2_msg = Auth_OpenID_Message::fromPostArgs($openid2_args); + $p = new Auth_OpenID_ServerError($openid2_msg, $error, + $reference, $contact); + $reply = $p->toMessage(); + + $this->assertEquals($reply->getArg(Auth_OpenID_OPENID_NS, 'reference'), + $reference); + $this->assertEquals($reply->getArg(Auth_OpenID_OPENID_NS, 'contact'), + $contact); + } + + function failUnlessExpiresInMatches($msg, $expected_expires_in) + { + $expires_in_str = $msg->getArg(Auth_OpenID_OPENID_NS, 'expires_in'); + if ($expires_in_str === null) { + $this->fail("Expected expires_in value."); + return; + } + + $expires_in = intval($expires_in_str); + + // Slop is necessary because the tests can sometimes get run + // right on a second boundary + $slop = 1; // second + $difference = $expected_expires_in - $expires_in; + + $error_message = sprintf('"expires_in" value not within %s of expected: '. + 'expected=%s, actual=%s', + $slop, $expected_expires_in, $expires_in); + $this->assertTrue((0 <= $difference && + $difference <= $slop), $error_message); + } + + function test_plaintext256() + { + if (defined('Auth_OpenID_NO_MATH_SUPPORT') || + !Auth_OpenID_SHA256_SUPPORTED) { + print "(Skipping test_plaintext256)"; + return; + } + + $this->assoc = $this->signatory->createAssociation(false, + 'HMAC-SHA256'); + $response = $this->request->answer($this->assoc); + $f = $response->fields; + + $this->assertEquals($f->getArg(Auth_OpenID_OPENID_NS, "assoc_type"), + "HMAC-SHA1"); + $this->assertEquals($f->getArg(Auth_OpenID_OPENID_NS, "assoc_handle"), + $this->assoc->handle); + + $this->failUnlessExpiresInMatches( + $f, + $this->signatory->SECRET_LIFETIME); + + $this->assertEquals( + $f->getArg(Auth_OpenID_OPENID_NS, "mac_key"), + base64_encode($this->assoc->secret)); + $this->assertFalse($f->hasKey(Auth_OpenID_OPENID_NS, "session_type")); + $this->assertFalse($f->hasKey(Auth_OpenID_OPENID_NS, "enc_mac_key")); + $this->assertFalse($f->hasKey(Auth_OpenID_OPENID_NS, "dh_server_public")); + } + + function test_unsupportedPrefer() + { + $allowed_assoc = 'COLD-PET-RAT'; + $allowed_sess = 'FROG-BONES'; + $message = 'This is a unit test'; + + // Set an OpenID 2 message so answerUnsupported doesn't raise + // ProtocolError. + $this->request->message = new Auth_OpenID_Message(Auth_OpenID_OPENID2_NS); + + $response = $this->request->answerUnsupported( + $message, + $allowed_assoc, + $allowed_sess); + $f = $response->fields; + $this->assertEquals($f->getArg(Auth_OpenID_OPENID_NS, 'error_code'), + 'unsupported-type'); + + $this->assertEquals($f->getArg(Auth_OpenID_OPENID_NS, 'assoc_type'), + $allowed_assoc); + + $this->assertEquals($f->getArg(Auth_OpenID_OPENID_NS, 'error'), + $message); + + $this->assertEquals($f->getArg(Auth_OpenID_OPENID_NS, 'session_type'), + $allowed_sess); + } + + function test_unsupported() + { + $message = 'This is a unit test'; + + $this->request->message = new Auth_OpenID_Message(Auth_OpenID_OPENID2_NS); + + $response = $this->request->answerUnsupported($message); + + $f = $response->fields; + $this->assertEquals($f->getArg(Auth_OpenID_OPENID_NS, 'error_code'), + 'unsupported-type'); + + $this->assertEquals($f->getArg(Auth_OpenID_OPENID_NS, 'assoc_type'), null); + $this->assertEquals($f->getArg(Auth_OpenID_OPENID_NS, 'error'), $message); + $this->assertEquals($f->getArg(Auth_OpenID_OPENID_NS, 'session_type'), null); + } +} + +class Counter { + function Counter() + { + $this->count = 0; + } + + function inc() + { + $this->count += 1; + } +} + +class Tests_Auth_OpenID_ServerTest extends PHPUnit_TestCase { + function setUp() + { + $this->store = new Tests_Auth_OpenID_MemStore(); + $this->server = new Auth_OpenID_Server($this->store); + } + + function test_associate() + { + if (!defined('Auth_OpenID_NO_MATH_SUPPORT')) { + $message = new Auth_OpenID_Message(Auth_OpenID_OPENID1_NS); + $request = Auth_OpenID_AssociateRequest::fromMessage($message); + $response = $this->server->openid_associate($request); + $this->assertTrue($response->fields->hasKey(Auth_OpenID_OPENID_NS, + 'assoc_handle')); + } + } + + function test_associate2() + { + // Associate when the server has no allowed association types + // + // Gives back an error with error_code and no fallback session + // or assoc types. + $this->server->negotiator->setAllowedTypes(array()); + + $msg = Auth_OpenID_Message::fromPostArgs(array( + 'openid.ns' => Auth_OpenID_OPENID2_NS, + 'openid.session_type' => 'no-encryption')); + + $request = Auth_OpenID_AssociateRequest::fromMessage($msg); + + $response = $this->server->openid_associate($request); + $this->assertTrue($response->fields->hasKey(Auth_OpenID_OPENID_NS, "error")); + $this->assertTrue($response->fields->hasKey(Auth_OpenID_OPENID_NS, "error_code")); + $this->assertFalse($response->fields->hasKey(Auth_OpenID_OPENID_NS, "assoc_handle")); + $this->assertFalse($response->fields->hasKey(Auth_OpenID_OPENID_NS, "assoc_type")); + $this->assertFalse($response->fields->hasKey(Auth_OpenID_OPENID_NS, "session_type")); + } + + function test_associate3() + { + if (defined('Auth_OpenID_NO_MATH_SUPPORT') || + !Auth_OpenID_HMACSHA256_SUPPORTED) { + print "(Skipping test_associate3)"; + return; + } + + // Request an assoc type that is not supported when there are + // supported types. + // + // Should give back an error message with a fallback type. + $this->server->negotiator->setAllowedTypes(array(array('HMAC-SHA256', 'DH-SHA256'))); + + $msg = Auth_OpenID_Message::fromPostArgs(array( + 'openid.ns' => Auth_OpenID_OPENID2_NS, + 'openid.session_type' => 'no-encryption')); + + $request = Auth_OpenID_AssociateRequest::fromMessage($msg); + $response = $this->server->openid_associate($request); + + $this->assertTrue($response->fields->hasKey(Auth_OpenID_OPENID_NS, "error")); + $this->assertTrue($response->fields->hasKey(Auth_OpenID_OPENID_NS, "error_code")); + $this->assertFalse($response->fields->hasKey(Auth_OpenID_OPENID_NS, "assoc_handle")); + $this->assertEquals($response->fields->getArg(Auth_OpenID_OPENID_NS, "assoc_type"), + 'HMAC-SHA256'); + $this->assertEquals($response->fields->getArg(Auth_OpenID_OPENID_NS, "session_type"), + 'DH-SHA256'); + } + + function test_associate4() + { + if (defined('Auth_OpenID_NO_MATH_SUPPORT') || + !Auth_OpenID_HMACSHA256_SUPPORTED) { + print "(Skipping test_associate4)"; + return; + } + + $this->assertTrue($this->server->negotiator->setAllowedTypes( + array(array('HMAC-SHA256', 'DH-SHA256')))); + + $query = array( + 'openid.dh_consumer_public' => + 'ALZgnx8N5Lgd7pCj8K86T/DDMFjJXSss1SKoLmxE72kJTzOtG6I2PaYrHX'. + 'xku4jMQWSsGfLJxwCZ6280uYjUST/9NWmuAfcrBfmDHIBc3H8xh6RBnlXJ'. + '1WxJY3jHd5k1/ZReyRZOxZTKdF/dnIqwF8ZXUwI6peV0TyS/K1fOfF/s', + 'openid.assoc_type' => 'HMAC-SHA256', + 'openid.session_type' => 'DH-SHA256'); + + $message = Auth_OpenID_Message::fromPostArgs($query); + $request = Auth_OpenID_AssociateRequest::fromMessage($message); + $response = $this->server->openid_associate($request); + $this->assertTrue($response->fields->hasKey(Auth_OpenID_OPENID_NS, "assoc_handle")); + } + + function test_missingSessionTypeOpenID2() + { + // Make sure session_type is required in OpenID 2 + $msg = Auth_OpenID_Message::fromPostArgs(array('openid.ns' => Auth_OpenID_OPENID2_NS)); + + $result = Auth_OpenID_AssociateRequest::fromMessage($msg); + + $this->assertTrue(is_a($result, 'Auth_OpenID_ServerError')); + } + + function test_checkAuth() + { + $request = new Auth_OpenID_CheckAuthRequest('arrrrrf', + '0x3999', array()); + + $response = $this->server->openid_check_authentication($request); + $this->assertTrue($response->fields->hasKey(Auth_OpenID_OPENID_NS, 'is_valid')); + } +} + +class Tests_Auth_OpenID_Signatory extends PHPUnit_TestCase { + function setUp() + { + $this->store =& new Tests_Auth_OpenID_MemStore(); + $this->signatory =& new Auth_OpenID_Signatory($this->store); + $this->dumb_key = $this->signatory->dumb_key; + $this->normal_key = $this->signatory->normal_key; + } + + function test_sign() + { + $request = new Auth_OpenID_ServerRequest(); + $request->namespace = Auth_OpenID_OPENID1_NS; + + $assoc_handle = '{assoc}{lookatme}'; + $assoc = Auth_OpenID_Association::fromExpiresIn(60, $assoc_handle, + 'sekrit', 'HMAC-SHA1'); + $this->store->storeAssociation($this->normal_key, $assoc); + $request->assoc_handle = $assoc_handle; + $request->namespace = Auth_OpenID_OPENID1_NS; + + $response = new Auth_OpenID_ServerResponse($request); + $response->fields = Auth_OpenID_Message::fromOpenIDArgs(array( + 'foo' => 'amsigned', + 'bar' => 'notsigned', + 'azu' => 'alsosigned')); + + $sresponse = $this->signatory->sign($response); + + $this->assertEquals($sresponse->fields->getArg(Auth_OpenID_OPENID_NS, + 'assoc_handle'), + $assoc_handle); + + $this->assertEquals($sresponse->fields->getArg(Auth_OpenID_OPENID_NS, 'signed'), + 'assoc_handle,azu,bar,foo,signed'); + + $this->assertTrue($sresponse->fields->hasKey(Auth_OpenID_OPENID_NS, 'sig')); + } + + function test_signDumb() + { + $request = new Auth_OpenID_ServerRequest(); + $request->assoc_handle = null; + $request->namespace = Auth_OpenID_OPENID1_NS; + + $response = new Auth_OpenID_ServerResponse($request); + $response->fields = Auth_OpenID_Message::fromOpenIDArgs(array( + 'foo' => 'amsigned', + 'bar' => 'notsigned', + 'azu' => 'alsosigned')); + + $sresponse = $this->signatory->sign($response); + + $assoc_handle = $sresponse->fields->getArg(Auth_OpenID_OPENID_NS, + 'assoc_handle'); + + $this->assertTrue($assoc_handle); + $assoc = $this->signatory->getAssociation($assoc_handle, true); + + $this->assertTrue($assoc); + $this->assertEquals($sresponse->fields->getArg(Auth_OpenID_OPENID_NS, 'signed'), + 'assoc_handle,azu,bar,foo,signed'); + $this->assertTrue($sresponse->fields->hasKey(Auth_OpenID_OPENID_NS, 'sig')); + } + + function test_signExpired() + { + $request = new Auth_OpenID_ServerRequest(); + $assoc_handle = '{assoc}{lookatme}'; + $assoc = Auth_OpenID_Association::fromExpiresIn(-10, $assoc_handle, + 'sekrit', 'HMAC-SHA1'); + $this->store->storeAssociation($this->normal_key, $assoc); + $this->assertTrue($this->store->getAssociation($this->normal_key, + $assoc_handle)); + + $request->assoc_handle = $assoc_handle; + $request->namespace = Auth_OpenID_OPENID1_NS; + + $response = new Auth_OpenID_ServerResponse($request); + $response->fields = Auth_OpenID_Message::fromOpenIDArgs(array( + 'foo' => 'amsigned', + 'bar' => 'notsigned', + 'azu' => 'alsosigned')); + + $sresponse = $this->signatory->sign($response); + + $new_assoc_handle = $sresponse->fields->getArg(Auth_OpenID_OPENID_NS, + 'assoc_handle'); + $this->assertTrue($new_assoc_handle); + $this->assertFalse($new_assoc_handle == $assoc_handle); + + $this->assertEquals($sresponse->fields->getArg(Auth_OpenID_OPENID_NS, + 'invalidate_handle'), + $assoc_handle); + + $this->assertEquals($sresponse->fields->getArg(Auth_OpenID_OPENID_NS, + 'signed'), + 'assoc_handle,azu,bar,foo,invalidate_handle,signed'); + $this->assertTrue($sresponse->fields->hasKey(Auth_OpenID_OPENID_NS, + 'sig')); + + // make sure the expired association is gone + $this->assertFalse($this->store->getAssociation($this->normal_key, + $assoc_handle)); + + // make sure the new key is a dumb mode association + $this->assertTrue($this->store->getAssociation($this->dumb_key, + $new_assoc_handle)); + + $this->assertFalse($this->store->getAssociation($this->normal_key, + $new_assoc_handle)); + } + + function test_signInvalidHandle() + { + $request = new Auth_OpenID_ServerRequest(); + $assoc_handle = '{bogus-assoc}{notvalid}'; + + $request->assoc_handle = $assoc_handle; + $request->namespace = Auth_OpenID_OPENID1_NS; + + $response = new Auth_OpenID_ServerResponse($request); + $response->fields = Auth_OpenID_Message::fromOpenIDArgs(array( + 'foo' => 'amsigned', + 'bar' => 'notsigned', + 'azu' => 'alsosigned')); + + $response->signed = array('foo', 'azu'); + $sresponse = $this->signatory->sign($response); + + $new_assoc_handle = $sresponse->fields->getArg(Auth_OpenID_OPENID_NS, + 'assoc_handle'); + + $this->assertTrue($new_assoc_handle); + $this->assertFalse($new_assoc_handle == $assoc_handle); + + $this->assertEquals($sresponse->fields->getArg(Auth_OpenID_OPENID_NS, + 'invalidate_handle'), + $assoc_handle); + + $this->assertEquals($sresponse->fields->getArg(Auth_OpenID_OPENID_NS, + 'signed'), + 'assoc_handle,azu,bar,foo,invalidate_handle,signed'); + $this->assertTrue($sresponse->fields->hasKey(Auth_OpenID_OPENID_NS, + 'sig')); + + // make sure the new key is a dumb mode association + $this->assertTrue($this->store->getAssociation($this->dumb_key, + $new_assoc_handle)); + + $this->assertFalse($this->store->getAssociation($this->normal_key, + $new_assoc_handle)); + } + + function test_verify() + { + $assoc_handle = '{vroom}{zoom}'; + $assoc = Auth_OpenID_Association::fromExpiresIn(60, $assoc_handle, + 'sekrit', 'HMAC-SHA1'); + + $this->store->storeAssociation($this->dumb_key, $assoc); + + $signed = Auth_OpenID_Message::fromPostArgs(array( + 'openid.foo' => 'bar', + 'openid.apple' => 'orange', + 'openid.assoc_handle' => $assoc_handle, + 'openid.signed' => 'apple,assoc_handle,foo,signed', + 'openid.sig' => 'uXoT1qm62/BB09Xbj98TQ8mlBco=')); + + $verified = $this->signatory->verify($assoc_handle, $signed); + $this->assertTrue($verified); + } + + function test_verifyBadSig() + { + $assoc_handle = '{vroom}{zoom}'; + $assoc = Auth_OpenID_Association::fromExpiresIn(60, $assoc_handle, + 'sekrit', 'HMAC-SHA1'); + + $this->store->storeAssociation($this->dumb_key, $assoc); + + $signed = Auth_OpenID_Message::fromPostArgs(array( + 'openid.foo' => 'bar', + 'openid.apple' => 'orange', + 'openid.assoc_handle' => $assoc_handle, + 'openid.signed' => 'apple,assoc_handle,foo,signed', + 'openid.sig' => str_rot13('uXoT1qm62/BB09Xbj98TQ8mlBco='))); + + $verified = $this->signatory->verify($assoc_handle, $signed); + + $this->assertFalse($verified); + } + + function test_verifyBadHandle() + { + $assoc_handle = '{vroom}{zoom}'; + $signed = Auth_OpenID_Message::fromPostArgs( + array('foo' => 'bar', + 'apple' => 'orange', + 'openid.sig' => "Ylu0KcIR7PvNegB/K41KpnRgJl0=")); + + $verified = $this->signatory->verify($assoc_handle, $signed); + $this->assertFalse($verified); + } + + function test_verifyAssocMismatch() + { + // Attempt to validate sign-all message with a signed-list + // assoc. + $assoc_handle = '{vroom}{zoom}'; + $assoc = Auth_OpenID_Association::fromExpiresIn( + 60, $assoc_handle, 'sekrit', 'HMAC-SHA1'); + + $this->store->storeAssociation($this->dumb_key, $assoc); + + $signed = Auth_OpenID_Message::fromPostArgs(array( + 'foo' => 'bar', + 'apple' => 'orange', + 'openid.sig' => "d71xlHtqnq98DonoSgoK/nD+QRM=" + )); + + $verified = $this->signatory->verify($assoc_handle, $signed); + $this->assertFalse($verified); + } + + function test_getAssoc() + { + $assoc_handle = $this->makeAssoc(true); + $assoc = $this->signatory->getAssociation($assoc_handle, true); + $this->assertTrue($assoc); + $this->assertEquals($assoc->handle, $assoc_handle); + } + + function test_getAssocExpired() + { + $assoc_handle = $this->makeAssoc(true, -10); + $assoc = $this->signatory->getAssociation($assoc_handle, true); + $this->assertFalse($assoc); + } + + function test_getAssocInvalid() + { + $ah = 'no-such-handle'; + $this->assertEquals( + $this->signatory->getAssociation($ah, false), null); + } + + function test_getAssocDumbVsNormal() + { + $assoc_handle = $this->makeAssoc(true); + $this->assertEquals( + $this->signatory->getAssociation($assoc_handle, false), null); + } + + function test_createAssociation() + { + $assoc = $this->signatory->createAssociation(false); + $this->assertTrue($this->signatory->getAssociation($assoc->handle, + false)); + } + + function makeAssoc($dumb, $lifetime = 60) + { + $assoc_handle = '{bling}'; + $assoc = Auth_OpenID_Association::fromExpiresIn( + $lifetime, $assoc_handle, + 'sekrit', 'HMAC-SHA1'); + + $this->store->storeAssociation((($dumb) ? $this->dumb_key : + $this->normal_key), $assoc); + return $assoc_handle; + } + + function test_invalidate() + { + $assoc_handle = '-squash-'; + $assoc = Auth_OpenID_Association::fromExpiresIn(60, $assoc_handle, + 'sekrit', 'HMAC-SHA1'); + + $this->store->storeAssociation($this->dumb_key, $assoc); + $assoc = $this->signatory->getAssociation($assoc_handle, true); + $this->assertTrue($assoc); + $assoc = $this->signatory->getAssociation($assoc_handle, true); + $this->assertTrue($assoc); + $this->signatory->invalidate($assoc_handle, true); + $assoc = $this->signatory->getAssociation($assoc_handle, true); + $this->assertFalse($assoc); + } +} + +class Tests_Auth_OpenID_Server extends PHPUnit_TestSuite { + + function getName() + { + return "Tests_Auth_OpenID_Server"; + } + + function Tests_Auth_OpenID_Server() + { + $this->addTestSuite('Tests_Auth_OpenID_Signatory'); + $this->addTestSuite('Tests_Auth_OpenID_ServerTest'); + if (!defined('Auth_OpenID_NO_MATH_SUPPORT')) { + $this->addTestSuite('Tests_Auth_OpenID_Associate'); + } + $this->addTestSuite('Tests_Auth_OpenID_CheckAuth'); + $this->addTestSuite('Tests_Auth_OpenID_CheckIDExtension'); + $this->addTestSuite('Tests_Auth_OpenID_CheckAuth'); + $this->addTestSuite('Tests_Auth_OpenID_SigningEncode'); + $this->addTestSuite('Tests_Auth_OpenID_Test_Encode'); + $this->addTestSuite('Tests_Auth_OpenID_Test_Decode'); + $this->addTestSuite('Tests_Auth_OpenID_Test_ServerError'); + $this->addTestSuite('Tests_Auth_OpenID_CheckID'); + } +} + +?> diff --git a/Tests/Auth/OpenID/StoreTest.php b/Tests/Auth/OpenID/StoreTest.php new file mode 100644 index 0000000..b656ba0 --- /dev/null +++ b/Tests/Auth/OpenID/StoreTest.php @@ -0,0 +1,705 @@ +<?php + +/** + * A test script for the OpenIDStore classes. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Require classes and functions to run the Store tests. + */ +require_once 'Auth/OpenID/Association.php'; +require_once 'Auth/OpenID/CryptUtil.php'; +require_once 'Auth/OpenID/Nonce.php'; +require_once 'Auth/OpenID.php'; +require_once 'PHPUnit.php'; + +function _Auth_OpenID_mkdtemp() +{ + if (strpos(PHP_OS, 'WIN') === 0) { + $dir = $_ENV['TMP']; + if (!isset($dir)) { + $dir = 'C:\Windows\Temp'; + } + } else { + $dir = @$_ENV['TMPDIR']; + if (!isset($dir)) { + $dir = '/tmp'; + } + } + + return Auth_OpenID_FileStore::_mkdtemp($dir); +} + +/** + * This is the host where the SQL stores' databases should be created + * and destroyed. + */ +global $_Auth_OpenID_db_test_host; +$_Auth_OpenID_db_test_host = 'dbtest'; + +/** + * Generate a sufficently unique database name so many hosts can run + * SQL store tests on the server at the same time and not step on each + * other. + */ +function _Auth_OpenID_getTmpDbName() +{ + $hostname = php_uname('n'); + $hostname = str_replace('.', '_', $hostname); + $hostname = str_replace('-', '_', $hostname); + $hostname = strtolower($hostname); + + return sprintf("%s_%d_%s_openid_test", + $hostname, + getmypid(), + strval(rand(1, time()))); +} + +/** + * Superclass that has methods for testing OpenID stores. Subclass this to + * test your own store implementation. + * + * @package OpenID + */ +class Tests_Auth_OpenID_Store extends PHPUnit_TestCase { + + /** + * Prepares for the SQL store tests. + */ + function setUp() + { + $this->letters = Auth_OpenID_letters; + $this->digits = Auth_OpenID_digits; + $this->punct = Auth_OpenID_punct; + $this->allowed_nonce = $this->letters . $this->digits; + $this->allowed_handle = $this->letters . $this->digits . $this->punct; + } + + /** + * Generates an association with the specified parameters. + */ + function genAssoc($now, $issued = 0, $lifetime = 600) + { + $sec = Auth_OpenID_CryptUtil::randomString(20); + $hdl = Auth_OpenID_CryptUtil::randomString(128, $this->allowed_handle); + return new Auth_OpenID_Association($hdl, $sec, $now + $issued, + $lifetime, 'HMAC-SHA1'); + } + + /** + * @access private + */ + function _checkRetrieve(&$store, $url, $handle, $expected, $name = null) + { + $retrieved_assoc = $store->getAssociation($url, $handle); + if ($expected === null) { + $this->assertTrue($retrieved_assoc === null); + } else { + $this->assertTrue($expected->equal($retrieved_assoc), $name); + } + } + + function _checkRemove(&$store, $url, $handle, $expected, $name = null) + { + $present = $store->removeAssociation($url, $handle); + $this->assertTrue((!$expected && !$present) || + ($expected && $present), + $name); + } + + /** + * Make sure a given store has a minimum of API compliance. Call + * this function with an empty store. + * + * Raises AssertionError if the store does not work as expected. + * + * OpenIDStore -> NoneType + */ + function _testStore(&$store) + { + // Association functions + $now = time(); + + $server_url = 'http://www.myopenid.com/openid'; + + $assoc = $this->genAssoc($now); + + $this->_checkRetrieve($store, $server_url, null, null, + 'Make sure that a missing association returns no result'); + + $store->storeAssociation($server_url, $assoc); + $this->_checkRetrieve($store, $server_url, null, $assoc, + 'Check that after storage, getting returns the same result'); + + $this->_checkRetrieve($store, $server_url, null, $assoc, + 'more than once'); + + $store->storeAssociation($server_url, $assoc); + $this->_checkRetrieve($store, $server_url, null, $assoc, + 'Storing more than once has no ill effect'); + + // Removing an association that does not exist returns not present + $this->_checkRemove($store, $server_url, $assoc->handle . 'x', false, + "Remove nonexistent association (1)"); + + // Removing an association that does not exist returns not present + $this->_checkRemove($store, $server_url . 'x', $assoc->handle, false, + "Remove nonexistent association (2)"); + + // Removing an association that is present returns present + $this->_checkRemove($store, $server_url, $assoc->handle, true, + "Remove existent association"); + + // but not present on subsequent calls + $this->_checkRemove($store, $server_url, $assoc->handle, false, + "Remove nonexistent association after removal"); + + // Put assoc back in the store + $store->storeAssociation($server_url, $assoc); + + // More recent and expires after assoc + $assoc2 = $this->genAssoc($now, $issued = 1); + $store->storeAssociation($server_url, $assoc2); + + $this->_checkRetrieve($store, $server_url, null, $assoc2, + 'After storing an association with a different handle, but the +same $server_url, the handle with the later expiration is +returned.'); + + $this->_checkRetrieve($store, $server_url, $assoc->handle, $assoc, + 'We can still retrieve the older association'); + + $this->_checkRetrieve($store, $server_url, $assoc2->handle, $assoc2, + 'Plus we can retrieve the association with the later expiration +explicitly'); + + $assoc3 = $this->genAssoc($now, $issued = 2, $lifetime = 100); + $store->storeAssociation($server_url, $assoc3); + + // More recent issued time, so assoc3 is expected. + $this->_checkRetrieve($store, $server_url, null, $assoc3, "(1)"); + + $this->_checkRetrieve($store, $server_url, $assoc->handle, + $assoc, "(2)"); + + $this->_checkRetrieve($store, $server_url, $assoc2->handle, + $assoc2, "(3)"); + + $this->_checkRetrieve($store, $server_url, $assoc3->handle, + $assoc3, "(4)"); + + $this->_checkRemove($store, $server_url, $assoc2->handle, true, "(5)"); + + $this->_checkRetrieve($store, $server_url, null, $assoc3, "(6)"); + + $this->_checkRetrieve($store, $server_url, $assoc->handle, + $assoc, "(7)"); + + $this->_checkRetrieve($store, $server_url, $assoc2->handle, + null, "(8)"); + + $this->_checkRetrieve($store, $server_url, $assoc3->handle, + $assoc3, "(9)"); + + $this->_checkRemove($store, $server_url, $assoc2->handle, + false, "(10)"); + + $this->_checkRemove($store, $server_url, $assoc3->handle, + true, "(11)"); + + $this->_checkRetrieve($store, $server_url, null, $assoc, "(12)"); + + $this->_checkRetrieve($store, $server_url, $assoc->handle, + $assoc, "(13)"); + + $this->_checkRetrieve($store, $server_url, $assoc2->handle, + null, "(14)"); + + $this->_checkRetrieve($store, $server_url, $assoc3->handle, + null, "(15)"); + + $this->_checkRemove($store, $server_url, $assoc2->handle, + false, "(16)"); + + $this->_checkRemove($store, $server_url, $assoc->handle, + true, "(17)"); + + $this->_checkRemove($store, $server_url, $assoc3->handle, + false, "(18)"); + + $this->_checkRetrieve($store, $server_url, null, null, "(19)"); + + $this->_checkRetrieve($store, $server_url, $assoc->handle, + null, "(20)"); + + $this->_checkRetrieve($store, $server_url, $assoc2->handle, + null, "(21)"); + + $this->_checkRetrieve($store, $server_url,$assoc3->handle, + null, "(22)"); + + $this->_checkRemove($store, $server_url, $assoc2->handle, + false, "(23)"); + + $this->_checkRemove($store, $server_url, $assoc->handle, + false, "(24)"); + + $this->_checkRemove($store, $server_url, $assoc3->handle, + false, "(25)"); + + // Put associations into store, for two different server URLs + $assoc1 = $this->genAssoc($now); + $assoc2 = $this->genAssoc($now + 2); + $server_url1 = "http://one.example.com/one"; + $server_url2 = "http://two.localhost.localdomain/two"; + + $store->storeAssociation($server_url1, $assoc1); + $store->storeAssociation($server_url2, $assoc2); + + // Ask for each one, make sure we get it + $this->_checkRetrieve($store, $server_url1, $assoc1->handle, + $assoc1, "(26)"); + + $this->_checkRetrieve($store, $server_url2, $assoc2->handle, + $assoc2, "(27)"); + + $store->storeAssociation($server_url1, $assoc1); + $store->storeAssociation($server_url2, $assoc2); + + // Ask for each one, make sure we get it + $this->_checkRetrieve($store, $server_url1, null, + $assoc1, "(28)"); + + $this->_checkRetrieve($store, $server_url2, null, + $assoc2, "(29)"); + + // test expired associations + // assoc 1: server 1, valid + // assoc 2: server 1, expired + // assoc 3: server 2, expired + // assoc 4: server 3, valid + $assocValid1 = $this->genAssoc($now, -3600, 7200); + $assocValid2 = $this->genAssoc($now, -5); + $assocExpired1 = $this->genAssoc($now, -7200, 3600); + $assocExpired2 = $this->genAssoc($now, -7200, 3600); + + if (!$store->supportsCleanup()) { + return; + } + + $store->cleanupAssociations(); + $store->storeAssociation($server_url . '1', $assocValid1); + $store->storeAssociation($server_url . '1', $assocExpired1); + $store->storeAssociation($server_url . '2', $assocExpired2); + $store->storeAssociation($server_url . '3', $assocValid2); + + $cleaned = $store->cleanupAssociations(); + $this->assertEquals(2, $cleaned); + } + + function _checkUseNonce(&$store, $nonce, $expected, $server_url, $msg=null) + { + list($stamp, $salt) = Auth_OpenID_splitNonce($nonce); + $actual = $store->useNonce($server_url, $stamp, $salt); + $this->assertEquals(intval($expected), intval($actual), "_checkUseNonce failed: $server_url, $msg"); + } + + function _testNonce(&$store) + { + // Nonce functions + + $server_url = 'http://www.myopenid.com/openid'; + + foreach (array($server_url, '') as $url) { + // Random nonce (not in store) + $nonce1 = Auth_OpenID_mkNonce(); + + // A nonce is not by default + $this->_checkUseNonce($store, $nonce1, true, $url, "blergx"); + + // Once stored, cannot be stored again + $this->_checkUseNonce($store, $nonce1, false, $url, 2); + + // And using again has the same effect + $this->_checkUseNonce($store, $nonce1, false, $url, 3); + + // Nonces from when the universe was an hour old should + // not pass these days. + $old_nonce = Auth_OpenID_mkNonce(3600); + $this->_checkUseNonce($store, $old_nonce, false, $url, + "Old nonce ($old_nonce) passed."); + + } + } + + function _testNonceCleanup(&$store) { + if (!$store->supportsCleanup()) { + return; + } + + $server_url = 'http://www.myopenid.com/openid'; + + $now = time(); + + $old_nonce1 = Auth_OpenID_mkNonce($now - 20000); + $old_nonce2 = Auth_OpenID_mkNonce($now - 10000); + $recent_nonce = Auth_OpenID_mkNonce($now - 600); + + global $Auth_OpenID_SKEW; + $orig_skew = $Auth_OpenID_SKEW; + + $Auth_OpenID_SKEW = 0; + $store->cleanupNonces(); + // Set SKEW high so stores will keep our nonces. + $Auth_OpenID_SKEW = 100000; + + $params = Auth_OpenID_splitNonce($old_nonce1); + array_unshift($params, $server_url); + $this->assertTrue(call_user_func_array(array(&$store, 'useNonce'), $params)); + + $params = Auth_OpenID_splitNonce($old_nonce2); + array_unshift($params, $server_url); + $this->assertTrue(call_user_func_array(array(&$store, 'useNonce'), $params)); + + $params = Auth_OpenID_splitNonce($recent_nonce); + array_unshift($params, $server_url); + $this->assertTrue(call_user_func_array(array(&$store, 'useNonce'), $params)); + + $Auth_OpenID_SKEW = 3600; + $cleaned = $store->cleanupNonces(); + $this->assertEquals(2, $cleaned); // , "Cleaned %r nonces." % (cleaned,) + + $Auth_OpenID_SKEW = 100000; + // A roundabout method of checking that the old nonces were + // cleaned is to see if we're allowed to add them again. + + $params = Auth_OpenID_splitNonce($old_nonce1); + array_unshift($params, $server_url); + $this->assertTrue(call_user_func_array(array(&$store, 'useNonce'), $params)); + $params = Auth_OpenID_splitNonce($old_nonce2); + array_unshift($params, $server_url); + $this->assertTrue(call_user_func_array(array(&$store, 'useNonce'), $params)); + + // The recent nonce wasn't cleaned, so it should still fail. + $params = Auth_OpenID_splitNonce($recent_nonce); + array_unshift($params, $server_url); + $this->assertFalse(call_user_func_array(array(&$store, 'useNonce'), $params)); + + $Auth_OpenID_SKEW = $orig_skew; + } + +} +/** + * Class that tests all of the stores included with the OpenID library + * + * @package OpenID + */ +class Tests_Auth_OpenID_Included_StoreTest extends Tests_Auth_OpenID_Store { + function test_memstore() + { + require_once 'Tests/Auth/OpenID/MemStore.php'; + $store = new Tests_Auth_OpenID_MemStore(); + $this->_testStore($store); + $this->_testNonce($store); + $this->_testNonceCleanup($store); + } + + function test_filestore() + { + require_once 'Auth/OpenID/FileStore.php'; + + $temp_dir = _Auth_OpenID_mkdtemp(); + + if (!$temp_dir) { + trigger_error('Could not create temporary directory ' . + 'with Auth_OpenID_FileStore::_mkdtemp', + E_USER_WARNING); + return null; + } + + $store = new Auth_OpenID_FileStore($temp_dir); + $this->_testStore($store); + $this->_testNonce($store); + $this->_testNonceCleanup($store); + $store->destroy(); + } + + function test_postgresqlstore() + { + // If the postgres extension isn't loaded or loadable, succeed + // because we can't run the test. + if (!(extension_loaded('pgsql') || + @dl('pgsql.so') || + @dl('php_pgsql.dll'))) { + print "(not testing PostGreSQL store)"; + $this->pass(); + return; + } + + require_once 'Auth/OpenID/PostgreSQLStore.php'; + require_once 'DB.php'; + + global $_Auth_OpenID_db_test_host; + + $temp_db_name = _Auth_OpenID_getTmpDbName(); + + $connect_db_name = 'test_master'; + + $dsn = array( + 'phptype' => 'pgsql', + 'username' => 'openid_test', + 'password' => '', + 'hostspec' => $_Auth_OpenID_db_test_host, + 'database' => $connect_db_name + ); + + $allowed_failures = 5; + $result = null; + $sleep_time = 1.0; + $sql = sprintf("CREATE DATABASE %s", $temp_db_name); + + for ($failures = 0; $failures < $allowed_failures; $failures++) { + $template_db =& DB::connect($dsn); + + if (PEAR::isError($template_db)) { + $result =& $template_db; + } else { + // Try to create the test database. + $result = $template_db->query($sql); + + $template_db->disconnect(); + unset($template_db); + + if (!PEAR::isError($result)) { + break; + } + } + + $sleep_time *= ((mt_rand(1, 100) / 100.0) + 1.5); + print "Failed to create database $temp_db_name.\n". + "Waiting $sleep_time before trying again\n"; + + $int_sleep = floor($sleep_time); + $frac_sleep = $sleep_time - $int_sleep; + sleep($int_sleep); + usleep($frac_sleep * 1000000.0); + } + + if ($failures == $allowed_failures) { + $this->pass("Temporary database creation failed after $failures ". + " tries ('$temp_db_name'): " . $result->getMessage()); + return; + } + + // Disconnect from template1 and reconnect to the temporary + // testing database. + $dsn['database'] = $temp_db_name; + $db =& DB::connect($dsn); + + if (PEAR::isError($db)) { + $this->fail("Temporary database connection failed " . + " ('$temp_db_name'): " . $db->getMessage()); + return; + } + + $store =& new Auth_OpenID_PostgreSQLStore($db); + + $this->assertFalse($store->tableExists($store->nonces_table_name)); + $this->assertFalse($store->tableExists($store->associations_table_name)); + + $store->createTables(); + + $this->assertTrue($store->tableExists($store->nonces_table_name)); + $this->assertTrue($store->tableExists($store->associations_table_name)); + + $this->_testStore($store); + $this->_testNonce($store); + $this->_testNonceCleanup($store); + + $db->disconnect(); + unset($db); + + // Connect to template1 again so we can drop the temporary + // database. + $dsn['database'] = $connect_db_name; + $template_db =& DB::connect($dsn); + + if (PEAR::isError($template_db)) { + $this->fail("Template database connection (to drop " . + "temporary database) failed: " . + $template_db->getMessage()); + return; + } + + $result = $template_db->query(sprintf("DROP DATABASE %s", + $temp_db_name)); + + if (PEAR::isError($result)) { + $this->fail("Dropping temporary database failed: " . + $result->getMessage()); + return; + } + + $template_db->disconnect(); + unset($template_db); + } + + function test_sqlitestore() + { + // If the sqlite extension isn't loaded or loadable, succeed + // because we can't run the test. + if (!(extension_loaded('sqlite') || + @dl('sqlite.so') || + @dl('php_sqlite.dll'))) { + print "(not testing SQLite store)"; + $this->pass(); + return; + } + + require_once 'Auth/OpenID/SQLiteStore.php'; + require_once 'DB.php'; + + $temp_dir = _Auth_OpenID_mkdtemp(); + + if (!$temp_dir) { + trigger_error('Could not create temporary directory ' . + 'with Auth_OpenID_FileStore::_mkdtemp', + E_USER_WARNING); + return null; + } + + $dsn = 'sqlite:///' . urlencode($temp_dir) . '/php_openid_storetest.db'; + $db =& DB::connect($dsn); + + if (PEAR::isError($db)) { + $this->pass("SQLite database connection failed: " . + $db->getMessage()); + } else { + $store =& new Auth_OpenID_SQLiteStore($db); + $this->assertTrue($store->createTables(), "Table creation failed"); + $this->_testStore($store); + $this->_testNonce($store); + $this->_testNonceCleanup($store); + } + + $db->disconnect(); + unset($db); + unset($store); + unlink($temp_dir . '/php_openid_storetest.db'); + rmdir($temp_dir); + } + + function test_mysqlstore() + { + // If the mysql extension isn't loaded or loadable, succeed + // because we can't run the test. + if (!(extension_loaded('mysql') || + @dl('mysql.' . PHP_SHLIB_SUFFIX))) { + print "(not testing MySQL store)"; + $this->pass(); + return; + } + + require_once 'Auth/OpenID/MySQLStore.php'; + require_once 'DB.php'; + + global $_Auth_OpenID_db_test_host; + + $dsn = array( + 'phptype' => 'mysql', + 'username' => 'openid_test', + 'password' => '', + 'hostspec' => $_Auth_OpenID_db_test_host + ); + + $db =& DB::connect($dsn); + + if (PEAR::isError($db)) { + print "MySQL database connection failed: " . + $db->getMessage(); + $this->pass(); + return; + } + + $temp_db_name = _Auth_OpenID_getTmpDbName(); + + $result = $db->query("CREATE DATABASE $temp_db_name"); + + if (PEAR::isError($result)) { + $this->pass("Error creating MySQL temporary database: " . + $result->getMessage()); + return; + } + + $db->query("USE $temp_db_name"); + + $store =& new Auth_OpenID_MySQLStore($db); + $store->createTables(); + $this->_testStore($store); + $this->_testNonce($store); + $this->_testNonceCleanup($store); + + $db->query("DROP DATABASE $temp_db_name"); + } +} + +/** + * This is the host that the store test will use + */ +global $_Auth_OpenID_memcache_test_host; +$_Auth_OpenID_memcache_test_host = 'localhost'; + +class Tests_Auth_OpenID_MemcachedStore_Test extends Tests_Auth_OpenID_Store { + function test_memcache() + { + // If the memcache extension isn't loaded or loadable, succeed + // because we can't run the test. + if (!(extension_loaded('memcache') || + @dl('memcache.so') || + @dl('php_memcache.dll'))) { + print "(skipping memcache store tests)"; + $this->pass(); + return; + } + require_once 'Auth/OpenID/MemcachedStore.php'; + + global $_Auth_OpenID_memcache_test_host; + + $memcached = new Memcache(); + if (!$memcached->connect($_Auth_OpenID_memcache_test_host)) { + print "(skipping memcache store tests - couldn't connect)"; + $this->pass(); + } else { + $store = new Auth_OpenID_MemcachedStore($memcached); + + $this->_testStore($store); + $this->_testNonce($store); + $this->_testNonceCleanup($store); + + $memcached->close(); + } + } +} + +class Tests_Auth_OpenID_StoreTest extends PHPUnit_TestSuite { + function getName() + { + return "Tests_Auth_OpenID_StoreTest"; + } + + function Tests_Auth_OpenID_StoreTest() + { + $this->addTestSuite('Tests_Auth_OpenID_Included_StoreTest'); + $this->addTestSuite('Tests_Auth_OpenID_MemcachedStore_Test'); + } +} +?> diff --git a/Tests/Auth/OpenID/TestUtil.php b/Tests/Auth/OpenID/TestUtil.php new file mode 100644 index 0000000..54b110d --- /dev/null +++ b/Tests/Auth/OpenID/TestUtil.php @@ -0,0 +1,62 @@ +<?php + +/** + * Utilites for test functions + */ + +require_once 'PHPUnit.php'; + +function Tests_Auth_OpenID_datafile($name, $reader) +{ + $path = dirname(realpath(__FILE__)); + $sep = DIRECTORY_SEPARATOR; + $filename = $path . $sep . 'data' . $sep . $name; + $data = $reader($filename); + if ($data === false) { + $msg = "Failed to open data file: $name"; + trigger_error($msg, E_USER_ERROR); + } + return $data; +} + +function Tests_Auth_OpenID_readdata($name) +{ + return Tests_Auth_OpenID_datafile($name, 'file_get_contents'); +} + +function Tests_Auth_OpenID_readlines($name) +{ + return Tests_Auth_OpenID_datafile($name, 'file'); +} + +class OpenIDTestMixin extends PHPUnit_TestCase { + function failUnlessOpenIDValueEquals($msg, $key, $expected, $ns=null) + { + if ($ns === null) { + $ns = Auth_OpenID_OPENID_NS; + } + + $actual = $msg->getArg($ns, $key); + $error_format = 'Wrong value for openid.%s: expected=%s, actual=%s'; + $error_message = sprintf($error_format, + $key, $expected, $actual); + + $this->assertEquals($expected, $actual, $error_message); + } + + function failIfOpenIDKeyExists($msg, $key, $ns=null) + { + if ($ns === null) { + $ns = Auth_OpenID_OPENID_NS; + } + + $actual = $msg->getArg($ns, $key); + $error_message = sprintf('openid.%s unexpectedly present: %s', + $key, $actual); + + $this->assertFalse($msg->hasKey($ns, $key), + $error_message); + } +} + +?>
\ No newline at end of file diff --git a/Tests/Auth/OpenID/TrustRoot.php b/Tests/Auth/OpenID/TrustRoot.php new file mode 100644 index 0000000..9c7c40e --- /dev/null +++ b/Tests/Auth/OpenID/TrustRoot.php @@ -0,0 +1,173 @@ +<?php + +/** + * Tests for the TrustRoot module + */ + +require_once "Auth/OpenID/TrustRoot.php"; +require_once "Tests/Auth/OpenID/TestUtil.php"; +require_once "PHPUnit.php"; + +class Tests_Auth_OpenID_TRParseCase extends PHPUnit_TestCase { + function Tests_Auth_OpenID_TRParseCase($desc, $case, $expected) + { + $this->setName($desc); + $this->case = $case; + $this->expected = $expected; + } + + function runTest() + { + $is_sane = Auth_OpenID_TrustRoot::isSane($this->case); + $parsed = (bool)Auth_OpenID_TrustRoot::_parse($this->case); + switch ($this->expected) { + case 'sane': + $this->assertTrue($parsed, "Did not parse"); + $this->assertTrue($is_sane, "Is not sane"); + break; + case 'insane': + $this->assertTrue($parsed, "Did not parse"); + $this->assertFalse($is_sane, "Is sane"); + break; + default: + $this->assertFalse($parsed, "Did parse"); + $this->assertFalse($is_sane, "Is sane"); + } + } +} + +class Tests_Auth_OpenID_TRMatchCase extends PHPUnit_TestCase { + function Tests_Auth_OpenID_TRMatchCase($desc, $tr, $rt, $matches) + { + $this->setName($desc); + $this->tr = $tr; + $this->rt = $rt; + $this->matches = $matches; + } + + function runTest() + { + $matches = Auth_OpenID_TrustRoot::match($this->tr, $this->rt); + $this->assertEquals((bool)$this->matches, (bool)$matches); + } +} + +function Tests_Auth_OpenID_parseHeadings($data, $c) +{ + $heading_pat = '/(^|\n)' . $c . '{40}\n([^\n]+)\n' . $c . '{40}\n()/'; + $offset = 0; + $headings = array(); + while (true) { + preg_match($heading_pat, substr($data, $offset), $matches, + PREG_OFFSET_CAPTURE); + if (!$matches) { + break; + } + $start = $matches[0][1]; + $heading = $matches[2][0]; + $end = $matches[3][1]; + $headings[] = array('heading' => $heading, + 'start' => $offset + $start, + 'end' => $offset + $end, + ); + $offset += $end; + } + return $headings; +} + +function Tests_Auth_OpenID_getSections($data) +{ + $headings = Tests_Auth_OpenID_parseHeadings($data, '-'); + $sections = array(); + $n = count($headings); + for ($i = 0; $i < $n; ) { + $secdata = $headings[$i]; + list($numtests, $desc) = explode(': ', $secdata['heading']); + $start = $secdata['end']; + $i += 1; + if ($i < $n) { + $blob = substr($data, $start, $headings[$i]['start'] - $start); + } else { + $blob = substr($data, $start); + } + $lines = explode("\n", trim($blob)); + if (count($lines) != $numtests) { + trigger_error('Parse failure: ' . var_export($secdata, true), + E_USER_ERROR); + } + $sections[] = array('desc' => $desc, 'lines' => $lines,); + } + return $sections; +} + +function Tests_Auth_OpenID_trParseTests($head, $tests) +{ + $tests = array('fail' => $tests[0], + 'insane' => $tests[1], + 'sane' => $tests[2]); + $testobjs = array(); + foreach ($tests as $expected => $testdata) { + $lines = $testdata['lines']; + foreach ($lines as $line) { + $desc = sprintf("%s - %s: %s", $head, + $testdata['desc'], var_export($line, true)); + $testobjs[] = new Tests_Auth_OpenID_TRParseCase( + $desc, $line, $expected); + } + } + return $testobjs; +} + +function Tests_Auth_OpenID_trMatchTests($head, $tests) +{ + $tests = array(true => $tests[0], false => $tests[1]); + $testobjs = array(); + foreach ($tests as $expected => $testdata) { + $lines = $testdata['lines']; + foreach ($lines as $line) { + $pat = '/^([^ ]+) +([^ ]+)$/'; + preg_match($pat, $line, $matches); + list($_, $tr, $rt) = $matches; + $desc = sprintf("%s - %s: %s %s", $head, $testdata['desc'], + var_export($tr, true), var_export($rt, true)); + $testobjs[] = new Tests_Auth_OpenID_TRMatchCase( + $desc, $tr, $rt, $expected); + } + } + return $testobjs; +} + +function Tests_Auth_OpenID_trustRootTests() +{ + $data = Tests_Auth_OpenID_readdata('trustroot.txt'); + list($parsehead, $matchhead) = Tests_Auth_OpenID_parseHeadings($data, '='); + $pe = $parsehead['end']; + $parsedata = substr($data, $pe, $matchhead['start'] - $pe); + $parsetests = Tests_Auth_OpenID_getSections($parsedata); + $parsecases = Tests_Auth_OpenID_trParseTests($parsehead['heading'], + $parsetests); + + $matchdata = substr($data, $matchhead['end']); + $matchtests = Tests_Auth_OpenID_getSections($matchdata); + $matchcases = Tests_Auth_OpenID_trMatchTests($matchhead['heading'], + $matchtests); + + return array_merge($parsecases, $matchcases); +} + +class Tests_Auth_OpenID_TrustRoot extends PHPUnit_TestSuite { + function Tests_Auth_OpenID_TrustRoot($name) + { + $this->setName($name); + + foreach (Tests_Auth_OpenID_trustRootTests() as $test) { + $this->_addTestByValue($test); + } + } + + function _addTestByValue($test) { + $this->addTest($test); + } +} + +?> diff --git a/Tests/Auth/OpenID/URINorm.php b/Tests/Auth/OpenID/URINorm.php new file mode 100644 index 0000000..a184034 --- /dev/null +++ b/Tests/Auth/OpenID/URINorm.php @@ -0,0 +1,68 @@ +<?php + +/** + * Tests for the URI normalization routines used by the OpenID + * library. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'PHPUnit.php'; +require_once 'Auth/OpenID/URINorm.php'; +require_once 'Tests/Auth/OpenID/TestUtil.php'; + +class Tests_Auth_OpenID_URINorm_TestCase extends PHPUnit_TestCase { + function Tests_Auth_OpenID_URINorm_TestCase( + $name, $uri, $expected) + { + + $this->setName($name); + $this->uri = $uri; + $this->expected = $expected; + } + + function runTest() + { + $actual = Auth_OpenID_urinorm($this->uri); + $this->assertEquals($this->expected, $actual); + } +} + +class Tests_Auth_OpenID_URINorm extends PHPUnit_TestSuite { + function _readTestCases() + { + $lines = Tests_Auth_OpenID_readlines('urinorm.txt'); + $cases = array(); + $case = array(); + for ($i = 0; $i < count($lines) && ($i + 3 <= count($lines)); $i += 4) { + $name = trim($lines[$i]); + $uri = trim($lines[$i + 1]); + $expected = trim($lines[$i + 2]); + if ($expected == 'fail') { + $expected = null; + } + $cases[] = array($name, $uri, $expected); + } + + return $cases; + } + + function Tests_Auth_OpenID_URINorm($name) + { + $this->setName($name); + $cases = $this->_readTestCases(); + foreach ($cases as $case) { + list($name, $uri, $expected) = $case; + $this->addTest(new Tests_Auth_OpenID_URINorm_TestCase($name, $uri, $expected)); + } + } +} + +?>
\ No newline at end of file diff --git a/Tests/Auth/OpenID/Util.php b/Tests/Auth/OpenID/Util.php new file mode 100644 index 0000000..a53da47 --- /dev/null +++ b/Tests/Auth/OpenID/Util.php @@ -0,0 +1,320 @@ +<?php + +/** + * Tests for utility functions used by the OpenID library. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'PHPUnit.php'; +require_once 'Auth/OpenID.php'; + +class Tests_Auth_OpenID_Util extends PHPUnit_TestCase { + function test_base64() + { + // This is not good for international use, but PHP doesn't + // appear to provide access to the local alphabet. + $letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + $digits = "0123456789"; + $extra = "+/="; + $allowed_s = $letters . $digits . $extra; + $allowed_d = array(); + + for ($i = 0; $i < strlen($allowed_s); $i++) { + $c = $allowed_s[$i]; + $allowed_d[$c] = null; + } + + function checkEncoded($obj, $str, $allowed_array) + { + for ($i = 0; $i < strlen($str); $i++) { + $obj->assertTrue(array_key_exists($str[$i], + $allowed_array)); + } + } + + $cases = array( + "", + "x", + "\x00", + "\x01", + str_repeat("\x00", 100), + implode("", array_map('chr', range(0, 255))) + ); + + foreach ($cases as $s) { + $b64 = base64_encode($s); + checkEncoded($this, $b64, $allowed_d); + $s_prime = base64_decode($b64); + $this->assertEquals($s_prime, $s); + } + + function random_ordinal($unused) + { + return rand(0, 255); + } + + // Randomized test + foreach (range(0, 49) as $i) { + $n = rand(0, 2048); + $s = implode("", array_map('chr', + array_map('random_ordinal', + range(0, $n)))); + $b64 = base64_encode($s); + checkEncoded($this, $b64, $allowed_d); + $s_prime = base64_decode($b64); + $this->assertEquals($s_prime, $s); + } + } + + function test_urldefrag() + { + $cases = array( + array('http://foo.com', 'http://foo.com'), + array('http://foo.com/', 'http://foo.com/'), + array('http://foo.com/path', 'http://foo.com/path'), + array('http://foo.com/path?query', 'http://foo.com/path?query'), + array('http://foo.com/path?query=v', 'http://foo.com/path?query=v'), + array('http://foo.com/?query=v', 'http://foo.com/?query=v'), + ); + + foreach ($cases as $pair) { + list($orig, $after) = $pair; + list($base, $frag) = Auth_OpenID::urldefrag($orig); + $this->assertEquals($after, $base); + $this->assertEquals($frag, ''); + + list($base, $frag) = Auth_OpenID::urldefrag($orig . "#fragment"); + $this->assertEquals($after, $base); + $this->assertEquals('fragment', $frag); + } + } + + function test_normalizeUrl() + { + $this->assertEquals("http://foo.com/", + Auth_OpenID::normalizeUrl("foo.com")); + + $this->assertEquals("http://foo.com/", + Auth_OpenID::normalizeUrl("http://foo.com")); + + $this->assertEquals("https://foo.com/", + Auth_OpenID::normalizeUrl("https://foo.com")); + + $this->assertEquals("http://foo.com/bar", + Auth_OpenID::normalizeUrl("foo.com/bar")); + + $this->assertEquals("http://foo.com/bar", + Auth_OpenID::normalizeUrl("http://foo.com/bar")); + + $this->assertEquals("http://foo.com/", + Auth_OpenID::normalizeUrl("http://foo.com/")); + + $this->assertEquals("https://foo.com/", + Auth_OpenID::normalizeUrl("https://foo.com/")); + + $this->assertEquals("https://foo.com/bar" , + Auth_OpenID::normalizeUrl("https://foo.com/bar")); + + $this->assertEquals("http://foo.com/bar" , + Auth_OpenID::normalizeUrl("HTtp://foo.com/bar")); + + $this->assertEquals("http://foo.com/bar" , + Auth_OpenID::normalizeUrl("HTtp://foo.com/bar#fraggle")); + + $this->assertEquals("http://foo.com/bAr/" , + Auth_OpenID::normalizeUrl("HTtp://fOo.com/bAr/.#fraggle")); + + if (0) { + $this->assertEquals("http://foo.com/%E8%8D%89", + Auth_OpenID::normalizeUrl("foo.com/\u8349")); + + $this->assertEquals("http://foo.com/%E8%8D%89", + Auth_OpenID::normalizeUrl("http://foo.com/\u8349")); + } + + $non_ascii_domain_cases = array( + array("http://xn--vl1a.com/", + "\u8349.com"), + + array("http://xn--vl1a.com/", + "http://\u8349.com"), + + array("http://xn--vl1a.com/", + "\u8349.com/"), + + array("http://xn--vl1a.com/", + "http://\u8349.com/"), + + array("http://xn--vl1a.com/%E8%8D%89", + "\u8349.com/\u8349"), + + array("http://xn--vl1a.com/%E8%8D%89", + "http://\u8349.com/\u8349"), + ); + + // XXX + /* + codecs.getencoder('idna') + except LookupError: + # If there is no idna codec, these cases with + # non-ascii-representable domain names should fail. + should_raise = True + else: + should_raise = False + + for expected, case in non_ascii_domain_cases: +try: +actual = Auth_OpenID::normalizeUrl(case) + except UnicodeError: + assert should_raise + else: +assert not should_raise and actual == expected, case + */ + + $this->assertNull(Auth_OpenID::normalizeUrl(null)); + $this->assertNull(Auth_OpenID::normalizeUrl('')); + $this->assertNull(Auth_OpenID::normalizeUrl('http://')); + } + + function test_appendArgs() + { + + $simple = 'http://www.example.com/'; + + $cases = array( + array('empty list', + array($simple, array()), + $simple), + + array('empty dict', + array($simple, array()), + $simple), + + array('one list', + array($simple, array(array('a', 'b'))), + $simple . '?a=b'), + + array('one dict', + array($simple, array('a' => 'b')), + $simple . '?a=b'), + + array('two list (same)', + array($simple, array(array('a', 'b'), + array('a', 'c'))), + $simple . '?a=b&a=c'), + + array('two list', + array($simple, array(array('a', 'b'), + array('b', 'c'))), + $simple . '?a=b&b=c'), + + array('two list (order)', + array($simple, array(array('b', 'c'), + array('a', 'b'))), + $simple . '?b=c&a=b'), + + array('two dict (order)', + array($simple, array('b' => 'c', + 'a' => 'b')), + $simple . '?a=b&b=c'), + + array('escape', + array($simple, array(array('=', '='))), + $simple . '?%3D=%3D'), + + array('escape (URL)', + array($simple, array(array('this_url', + $simple))), + $simple . + '?this_url=http%3A%2F%2Fwww.example.com%2F'), + + array('use dots', + array($simple, array(array('openid.stuff', + 'bother'))), + $simple . '?openid.stuff=bother'), + + array('args exist (empty)', + array($simple . '?stuff=bother', array()), + $simple . '?stuff=bother'), + + array('args exist', + array($simple . '?stuff=bother', + array(array('ack', 'ack'))), + $simple . '?stuff=bother&ack=ack'), + + array('args exist', + array($simple . '?stuff=bother', + array(array('ack', 'ack'))), + $simple . '?stuff=bother&ack=ack'), + + array('args exist (dict)', + array($simple . '?stuff=bother', + array('ack' => 'ack')), + $simple . '?stuff=bother&ack=ack'), + + array('args exist (dict 2)', + array($simple . '?stuff=bother', + array('ack' => 'ack', 'zebra' => 'lion')), + $simple . '?stuff=bother&ack=ack&zebra=lion'), + + array('three args (dict)', + array($simple, array('stuff' => 'bother', + 'ack' => 'ack', + 'zebra' => 'lion')), + $simple . '?ack=ack&stuff=bother&zebra=lion'), + + array('three args (list)', + array($simple, array( + array('stuff', 'bother'), + array('ack', 'ack'), + array('zebra', 'lion'))), + $simple . '?stuff=bother&ack=ack&zebra=lion'), + ); + + // Tests. + foreach ($cases as $case) { + list($desc, $data, $expected) = $case; + list($url, $query) = $data; + $this->assertEquals($expected, + Auth_OpenID::appendArgs($url, $query)); + } + } + + function test_getQuery() + { + $queries = array( + '' => array(), + 'single' => array(), + 'no&pairs' => array(), + 'x%3Dy' => array(), + 'single&real=value' => array('real' => 'value'), + 'x=y&m=x%3Dn' => array('x' => 'y', 'm' => 'x=n'), + '&m=x%20y' => array('m' => 'x y'), + 'single&&m=x%20y&bogus' => array('m' => 'x y'), + // Even with invalid encoding. But don't do that. + 'too=many=equals&' => array('too' => 'many=equals') + ); + + foreach ($queries as $s => $data) { + $query = Auth_OpenID::getQuery($s); + + foreach ($data as $key => $value) { + $this->assertTrue($query[$key] === $value); + } + + foreach ($query as $key => $value) { + $this->assertTrue($data[$key] === $value); + } + } + } +} + +?> diff --git a/Tests/Auth/OpenID/VerifyDisco.php b/Tests/Auth/OpenID/VerifyDisco.php new file mode 100644 index 0000000..f3b63a3 --- /dev/null +++ b/Tests/Auth/OpenID/VerifyDisco.php @@ -0,0 +1,424 @@ +<?php + +require_once "PHPUnit.php"; +require_once "Tests/Auth/OpenID/TestUtil.php"; +require_once "Tests/Auth/OpenID/MemStore.php"; + +require_once "Auth/OpenID/Message.php"; +require_once "Auth/OpenID/Consumer.php"; + +class Tests_Auth_OpenID_VerifyDisco_1 extends Auth_OpenID_GenericConsumer { + function _discoverAndVerify($claimed_id, $to_match_endpoints) + { + $this->test_case->assertEquals($this->endpoint->claimed_id, $claimed_id); + return new Auth_OpenID_FailureResponse(null, $this->text); + } +} + +class __VerifiedError extends Auth_OpenID_FailureResponse { +} + +class VerifyDisco_Consumer_verifiedError extends Auth_OpenID_GenericConsumer { + function _discoverAndVerify($to_match) + { + return new __VerifiedError(null, 'verified error'); + } +} + +class _DiscoverAndVerify extends OpenIDTestMixin { + var $consumer_class = 'Auth_OpenID_GenericConsumer'; + + function setUp() + { + $this->store = new Tests_Auth_OpenID_MemStore(); + $cl = $this->consumer_class; + $this->consumer = new $cl($this->store); + $this->return_to = "http://some.host/path"; + $this->endpoint = new Auth_OpenID_ServiceEndpoint(); + + $this->server_id = "sirod"; + $this->server_url = "serlie"; + $this->consumer_id = "consu"; + + $this->endpoint->claimed_id = $this->consumer_id; + $this->endpoint->server_url = $this->server_url; + $this->endpoint->local_id = $this->server_id; + $this->endpoint->type_uris = array(Auth_OpenID_TYPE_1_1); + } + + function failUnlessProtocolError($thing) + { + $this->assertTrue(Auth_OpenID::isFailure($thing)); + } +} + +class _Tests_discoveryOverride { + function _Tests_discoveryOverride($endpoint) + { + $this->endpoint = $endpoint; + } + + function discover($unused_url) + { + return array($this->endpoint->claimed_id, array($this->endpoint)); + } +} +class Tests_openID1Fallback1_0 extends _DiscoverAndVerify { + function test_openID1Fallback1_0() + { + $claimed_id = 'http://claimed.id/'; + $resp_msg = Auth_OpenID_Message::fromOpenIDArgs( + array('ns' => Auth_OpenID_OPENID1_NS, + 'identity' => $claimed_id)); + $resp_msg->setArg(Auth_OpenID_BARE_NS, 'openid1_claimed_id', + $claimed_id); + $expected_endpoint = new Auth_OpenID_ServiceEndpoint(); + $expected_endpoint->type_uris = array(Auth_OpenID_TYPE_1_0); + $expected_endpoint->local_id = null; + $expected_endpoint->claimed_id = $claimed_id; + + $discovery_override = new _Tests_discoveryOverride($expected_endpoint); + $this->consumer->discoverMethod = array($discovery_override, 'discover'); + + $actual_endpoint = $this->consumer->_verifyDiscoveryResults( + $resp_msg, null); + + $this->assertTrue(is_a($actual_endpoint, "Auth_OpenID_ServiceEndpoint")); + + $this->assertEquals($expected_endpoint->local_id, + $actual_endpoint->local_id); + $this->assertEquals($expected_endpoint->server_url, + $actual_endpoint->server_url); + + $this->assertEquals($expected_endpoint->type_uris, + $actual_endpoint->type_uris); + + $this->assertEquals($expected_endpoint->claimed_id, + $actual_endpoint->claimed_id); + + } +} + +class Tests_Auth_OpenID_VerifyDisco extends _DiscoverAndVerify { + function test_openID1NoLocalID() + { + $endpoint = new Auth_OpenID_ServiceEndpoint(); + $endpoint->claimed_id = 'bogus'; + + $msg = Auth_OpenID_Message::fromOpenIDArgs(array()); + // 'Missing required field openid.identity' + $this->failUnlessProtocolError($this->consumer->_verifyDiscoveryResults($msg, $endpoint)); + } + + function test_openID1NoEndpoint() + { + $msg = Auth_OpenID_Message::fromOpenIDArgs(array('identity' => 'snakes on a plane')); + $this->failUnlessProtocolError($this->consumer->_verifyDiscoveryResults($msg)); + } + + function test_openID2NoOPEndpointArg() + { + $msg = Auth_OpenID_Message::fromOpenIDArgs(array('ns' => Auth_OpenID_OPENID2_NS)); + $this->failUnlessProtocolError($this->consumer->_verifyDiscoveryResults($msg, null)); + } + + function test_openID2LocalIDNoClaimed() + { + $msg = Auth_OpenID_Message::fromOpenIDArgs(array('ns' => Auth_OpenID_OPENID2_NS, + 'op_endpoint' => 'Phone Home', + 'identity' => 'Jose Lius Borges')); + // 'openid.identity is present without', + $this->failUnlessProtocolError($this->consumer->_verifyDiscoveryResults($msg)); + } + + function test_openID2NoLocalIDClaimed() + { + $msg = Auth_OpenID_Message::fromOpenIDArgs(array('ns' => Auth_OpenID_OPENID2_NS, + 'op_endpoint' => 'Phone Home', + 'claimed_id' => 'Manuel Noriega')); + // 'openid.claimed_id is present without', + $this->failUnlessProtocolError( + $this->consumer->_verifyDiscoveryResults($msg)); + } + + function test_openID2NoIdentifiers() + { + $op_endpoint = 'Phone Home'; + $msg = Auth_OpenID_Message::fromOpenIDArgs(array('ns' => Auth_OpenID_OPENID2_NS, + 'op_endpoint' => $op_endpoint)); + $result_endpoint = $this->consumer->_verifyDiscoveryResults($msg); + $this->assertTrue($result_endpoint->isOPIdentifier()); + $this->assertEquals($op_endpoint, $result_endpoint->server_url); + $this->assertEquals(null, $result_endpoint->claimed_id); + } + + function test_openid2UsePreDiscovered() + { + $endpoint = new Auth_OpenID_ServiceEndpoint(); + $endpoint->local_id = 'my identity'; + $endpoint->claimed_id = 'i am sam'; + $endpoint->server_url = 'Phone Home'; + $endpoint->type_uris = array(Auth_OpenID_TYPE_2_0); + + $msg = Auth_OpenID_Message::fromOpenIDArgs( + array('ns' => Auth_OpenID_OPENID2_NS, + 'identity' => $endpoint->local_id, + 'claimed_id' => $endpoint->claimed_id, + 'op_endpoint' => $endpoint->server_url)); + + $result = $this->consumer->_verifyDiscoveryResults($msg, $endpoint); + $this->assertTrue($result === $endpoint); + } + + function test_openid2UsePreDiscoveredWrongType() + { + $this->consumer =& new Tests_Auth_OpenID_VerifyDisco_1($this->store); + $this->consumer->test_case =& $this; + $this->consumer->text = "verify failed"; + + $endpoint = new Auth_OpenID_ServiceEndpoint(); + $endpoint->local_id = 'my identity'; + $endpoint->claimed_id = 'i am sam'; + $endpoint->server_url = 'Phone Home'; + $endpoint->type_uris = array(Auth_OpenID_TYPE_1_1); + + $this->consumer->endpoint =& $endpoint; + + $msg = Auth_OpenID_Message::fromOpenIDArgs( + array('ns' => Auth_OpenID_OPENID2_NS, + 'identity' => $endpoint->local_id, + 'claimed_id' => $endpoint->claimed_id, + 'op_endpoint' => $endpoint->server_url)); + + $result = $this->consumer->_verifyDiscoveryResults($msg, $endpoint); + $this->failUnlessProtocolError($result); + $this->assertTrue($result->message == "verify failed"); + } + + function test_openid1UsePreDiscovered() + { + $endpoint = new Auth_OpenID_ServiceEndpoint(); + $endpoint->local_id = 'my identity'; + $endpoint->claimed_id = 'i am sam'; + $endpoint->server_url = 'Phone Home'; + $endpoint->type_uris = array(Auth_OpenID_TYPE_1_1); + + $msg = Auth_OpenID_Message::fromOpenIDArgs( + array('ns' => Auth_OpenID_OPENID1_NS, + 'identity' => $endpoint->local_id)); + $result = $this->consumer->_verifyDiscoveryResults($msg, $endpoint); + $this->assertTrue($result == $endpoint); + } + + function test_openid2Fragment() + { + $claimed_id = "http://unittest.invalid/"; + $claimed_id_frag = $claimed_id . "#fragment"; + + $endpoint = new Auth_OpenID_ServiceEndpoint(); + $endpoint->local_id = 'my identity'; + $endpoint->claimed_id = $claimed_id; + $endpoint->server_url = 'Phone Home'; + $endpoint->type_uris = array(Auth_OpenID_TYPE_2_0); + + $msg = Auth_OpenID_Message::fromOpenIDArgs( + array('ns' => Auth_OpenID_OPENID2_NS, + 'identity' => $endpoint->local_id, + 'claimed_id' => $claimed_id_frag, + 'op_endpoint' => $endpoint->server_url)); + $result = $this->consumer->_verifyDiscoveryResults($msg, $endpoint); + + $this->assertEquals($result->local_id, $endpoint->local_id); + $this->assertEquals($result->server_url, $endpoint->server_url); + + $this->assertEquals($result->type_uris, $endpoint->type_uris); + + $this->assertEquals($result->claimed_id, $claimed_id_frag); + } + +} + +class Tests_openid1UsePreDiscoveredWrongType extends _DiscoverAndVerify { + var $consumer_class = 'VerifyDisco_Consumer_verifiedError'; + + function test_openid1UsePreDiscoveredWrongType() + { + $endpoint = new Auth_OpenID_ServiceEndpoint(); + $endpoint->local_id = 'my identity'; + $endpoint->claimed_id = 'i am sam'; + $endpoint->server_url = 'Phone Home'; + $endpoint->type_uris = array(Auth_OpenID_TYPE_2_0); + + $msg = Auth_OpenID_Message::fromOpenIDArgs( + array('ns' => Auth_OpenID_OPENID1_NS, + 'identity' => $endpoint->local_id)); + + $result = $this->consumer->_verifyDiscoveryResults($msg, $endpoint); + $this->failUnlessProtocolError($result); + $this->assertTrue(is_a($result, '__VerifiedError')); + } +} + +// XXX: test the implementation of _discoverAndVerify + +class Tests_openID2NoEndpointDoesDisco_sentinel extends Auth_OpenID_GenericConsumer { + var $sentinel = 'blah'; + + function _discoverAndVerify($to_match) + { + return $this->sentinel; + } +} + +class Tests_openID2NoEndpointDoesDisco_failure extends Auth_OpenID_GenericConsumer { + var $failure_message = 'A fake failure response message'; + + function _verifyDiscoverySingle($to_match) + { + return new Auth_OpenID_FailureResponse(null, $this->failure_message); + } +} + +class Tests_openID2NoEndpointDoesDisco extends Tests_Auth_OpenID_VerifyDisco { + var $consumer_class = 'Tests_openID2NoEndpointDoesDisco_sentinel'; + + function test_openID2NoEndpointDoesDisco() + { + $op_endpoint = 'Phone Home'; + $this->consumer->sentinel = new Auth_OpenID_ServiceEndpoint(); + $this->consumer->sentinel->claimed_id = 'monkeysoft'; + + $msg = Auth_OpenID_Message::fromOpenIDArgs( + array('ns' => Auth_OpenID_OPENID2_NS, + 'identity' => 'sour grapes', + 'claimed_id' => 'monkeysoft', + 'op_endpoint' => $op_endpoint)); + + $result = $this->consumer->_verifyDiscoveryResults($msg); + $this->assertEquals($this->consumer->sentinel, $result); + } +} + +class Tests_openID2MismatchedDoesDisco extends Tests_Auth_OpenID_VerifyDisco { + var $consumer_class = 'Tests_openID2NoEndpointDoesDisco_sentinel'; + + function test_openID2MismatchedDoesDisco() + { + $mismatched = new Auth_OpenID_ServiceEndpoint(); + $mismatched->identity = 'nothing special, but different'; + $mismatched->local_id = 'green cheese'; + + $sentinel = new Auth_OpenID_ServiceEndpoint(); + $sentinel->claimed_id = 'monkeysoft'; + $this->consumer->sentinel = $sentinel; + + $op_endpoint = 'Phone Home'; + + $msg = Auth_OpenID_Message::fromOpenIDArgs( + array('ns' => Auth_OpenID_OPENID2_NS, + 'identity' => 'sour grapes', + 'claimed_id' => 'monkeysoft', + 'op_endpoint' => $op_endpoint)); + + $result = $this->consumer->_verifyDiscoveryResults($msg, $mismatched); + $this->assertEquals($this->consumer->sentinel, $result); + } +} + +class Tests_openID2MismatchedDoesDisco_failure extends PHPUnit_TestCase { + var $consumer_class = 'Tests_openID2NoEndpointDoesDisco_failure'; + + function setUp() + { + $this->store = new Tests_Auth_OpenID_MemStore(); + $cl = $this->consumer_class; + $this->consumer = new $cl($this->store); + $this->return_to = "http://some.host/path"; + $this->endpoint = new Auth_OpenID_ServiceEndpoint(); + + $this->consumer->discoverMethod = array($this, "_getServices"); + + $this->server_id = "sirod"; + $this->server_url = "serlie"; + $this->consumer_id = "consu"; + + $this->endpoint->claimed_id = $this->consumer_id; + $this->endpoint->server_url = $this->server_url; + $this->endpoint->local_id = $this->server_id; + $this->endpoint->type_uris = array(Auth_OpenID_TYPE_1_1); + } + + function _getServices($claimed_id, $fetcher=null) { + return array(null, array($this->endpoint)); + } + + function test_openID2MismatchedDoesDisco_failure() + { + $mismatched = new Auth_OpenID_ServiceEndpoint(); + $mismatched->identity = 'nothing special, but different'; + $mismatched->local_id = 'green cheese'; + + $op_endpoint = 'Phone Home'; + + $msg = Auth_OpenID_Message::fromOpenIDArgs( + array('ns' => Auth_OpenID_OPENID2_NS, + 'identity' => 'sour grapes', + 'claimed_id' => 'monkeysoft', + 'op_endpoint' => $op_endpoint)); + + $result = $this->consumer->_verifyDiscoveryResults($msg, $mismatched); + $this->assertTrue(Auth_OpenID::isFailure($result)); + } +} + +class TestVerifyDiscoverySingle extends OpenIDTestMixin { + var $consumer_class = 'Auth_OpenID_GenericConsumer'; + + function setUp() + { + $this->store = new Tests_Auth_OpenID_MemStore(); + $cl = $this->consumer_class; + $this->consumer = new $cl($this->store); + $this->return_to = "http://some.host/path"; + $this->endpoint = new Auth_OpenID_ServiceEndpoint(); + + $this->server_id = "sirod"; + $this->server_url = "serlie"; + $this->consumer_id = "consu"; + + $this->endpoint->claimed_id = $this->consumer_id; + $this->endpoint->server_url = $this->server_url; + $this->endpoint->local_id = $this->server_id; + $this->endpoint->type_uris = array(Auth_OpenID_TYPE_1_1); + } + + function test_endpointWithoutLocalID() + { + // An endpoint like this with no local_id is generated as a + // result of e.g. Yadis discovery with no LocalID tag. + $endpoint = new Auth_OpenID_ServiceEndpoint(); + $endpoint->server_url = "http://localhost:8000/openidserver"; + $endpoint->claimed_id = "http://localhost:8000/id/id-jo"; + + $to_match = new Auth_OpenID_ServiceEndpoint(); + $to_match->server_url = "http://localhost:8000/openidserver"; + $to_match->claimed_id = "http://localhost:8000/id/id-jo"; + $to_match->local_id = "http://localhost:8000/id/id-jo"; + + $result = $this->consumer->_verifyDiscoverySingle($endpoint, $to_match); + + // result should always be None, raises exception on failure. + $this->assertEquals($result, null); + } +} + +global $Tests_Auth_OpenID_VerifyDisco_other; +$Tests_Auth_OpenID_VerifyDisco_other = array( + new Tests_openID2MismatchedDoesDisco(), + new Tests_openID2NoEndpointDoesDisco(), + new Tests_openID2MismatchedDoesDisco_failure(), + new Tests_openid1UsePreDiscoveredWrongType(), + new Tests_openID1Fallback1_0(), + ); + +?>
\ No newline at end of file diff --git a/Tests/Auth/OpenID/data/dhexch b/Tests/Auth/OpenID/data/dhexch new file mode 100644 index 0000000..7a8be07 --- /dev/null +++ b/Tests/Auth/OpenID/data/dhexch @@ -0,0 +1,25 @@ +2126517416 1503105722 107994742031567165457540274858809652167995244913839787589743578103625285208352685332617657612917013907706708046204110421143061814711041523408378123857885283119340960531860106377561159933747570456852792031929014142490977574671361334052940195520731561436107832724927338342871107723160776334443535305873323500256 +1726325708 97982531 120276049194830643562108817809662062106546923236483207874835831104433621725428405166395533625121884325430201809382464614237831903082554679507511419420917850293136497504021313018779166242697363853538302653639224106865664333535348491338293646599577557630201300627170167862878616413985927131488374258664616392092 +7191572 1822336109 122056637146069839485450157659463602136773435897258591538866604089577736061707581662949502933519623787237101424065925246227311788026398025228549571401681364284397681558495127293422405005519207011429758784679359026564663723906503072635741603815702201571462971153413211041068663899032199555395016545688330586044 +228786056 1675584603 117701151389970889003978379762267752133865163176557074701079147801046451813688145274964215828365957142552336288689654120601548113748929766366564174096775189852190965077394508456052823068488823291767811224538103381867631753777200820579564678159674117155563703553000884461777658090456283332220216734612556688513 +804908984 2135801843 64993745755656098807721600357147815184322152457013249228399624894951891365784451431980303960531348451816046446458097670030866575746640795583720668211568084611960590087205609635406810868042628237527172170079769791670793545166757565720288711565725471066283638538704293790079806106677373567711048953361333211210 +1122280790 1614259350 3970244390792364343312138669070883638127263175075582263096706481850448381703964455173288740017450440943500797144425033043394229962284552755667989452357422108026327690718801740503572676309934059296211509079412739861064178751353597300902277808636740745335474263237517162977595705012834985895341553367459792583 +554314515 998420920 123643361743671701825618173162391028658772950477687990712748798605914570427945055208640384218469111438408345424338893652923941552893606133066783001743721804859308011765507616515353882559991735865794891472781955621601467016660304188272123202130977476560852093272543539966026915472638392018462692756028817601255 +719945347 612820861 103551249110130410018728043034553056272870332237608830696804104933749752848182147616875273399120950956495972830177071656956289995248469136767050516935071277259217339496102844766065836859503647533631994664364053659677674596380230790347281302210808329346735637394258018381272973124437749621859047096780954428763 +1030625109 1519412381 15696617275088442746387785148060623054817901281945029743077460769180096631404983383910114661025034557230747207080210469011273591184527303007260363112037932265980126744700626024259985586947347501172863220738584453809309462129610346067407238209251289710742647393829531889303218223237301268534338307710560528439 +1711254768 1710707291 57671766580359527580383190392835992822988951359063099518333951473557157636791373831886967456320589708220219137556141104065094734173377513568373511612097050435132426608350879787688784646390242899955969071718348216328885834450215105058725433533776719158074043390881257587614495125963197168525883771897032429145 +350065369 319208735 44521456496863698728911963510653524876630475042295240074435222668882607096381437705850621136342821688618111659046306438623837465097724847737566157513351593063095577843263064035230462006868686576892037899378382406468120801597507834123281075093108794208153836881908434178839513489161646768450411118658866064760 +2060218994 1946043223 56312469398022909670236635086334904553302665496424591277087996113064393075310693844995965663947160222486178761741935506327835516277624460430181450292802834360724976701838361338230904004764511115279873515265325364671729600765057941485718305803874368460265173324375012707038078949514720443784809672434739391394 +348859478 486047271 63578529904279717544096579659734885121575283886072626718230632949600891106018451131129915865157675764791879663149288069798959505461654979937263948081560345222746334083402817181164255208790802816536212306902000509334761465449621953806270899950736579351124776383450108496252367170418747525612150563944975123906 +1012847796 1311216437 107243486601777772242638374457577339776317528440551074937135087437181884726459082109032187432358497015564158022857522392034581988349463322793155498885898276448910563934149930379053835676169014345745737841013305105289515557002942278171260275549569040668192618881235525671100756834785472005323827534045854021808 +1108188659 73002956 151810407586486667408500610375120927048683500513617431444670840241105574837701928593342245869128797827409414723271900961746330458448581633550115101600384307415146567797051023727766743006773542272526168313129133103058023736384944187039543948615045687254043611794926502253898300807539332514119558228945387167129 +1367891527 957164137 106888874248390844568539366153235739322099571679913873665077300044384432133087328354115380340807163549209282323027334598550750155549975114208460003069900549945106924101337174939911191001778776920788324219234143471273850920009578258747072782983631129326451246547584416492036977756842649955247390532642313031673 +1109319782 312705549 68015190517529836158634537187194145539209089151286211608940739126031607591236786876471227901028349157506546942329120582174574792522929377067808957872180985535362179866434055727341930725758279092655515659945664689974113139170079360543337269872976473433045950679959300152245802435127006127508284128226462808242 +77629902 1442208847 80002290434058357698705037287975366182731667140415670086832039653095542318592553515737200055739316505804591069679885064388918352070456242079053263046801427045473800954002156858264359891351667747947419377687560365442620710551484084591458509139700723197713694698034159851521977928611736821392017020477832037627 +1876665427 42392232 94638321177007894302596716790742601595610267055803051893624262442254201910985079684859058170169970420428917385076321338591735591117741499259022286688741758915429225227910702796255294628145448897362890044237980198958757175908654342104958253051607028469935368687936664986431151922321609065078691893879183189566 +559635525 1782490275 71365295096950865667427967092027725943054589808884646377867956234326896501650860934260905567087143525158504721587985301638221372965891635800328428504369765880956526297788284176796001565732103141822914955442743632126166019769189834699258461912602048002960149362882398622111007162709444738907309082349930416022 +743575094 32085276 110453584602878746921666429659148701413696049424461554973529870857842263810553552249241246313332783204009612578170167391820378603413812193480492843470042238103670641705732755583940134259794253885826115668806244330875357074130716388274584300227667628005544555311079499447940768673150499033922449576383527638186 +129818206 137481306 140835473021876998102027624369898079740454145360699735493249477450544517213808389172240396819192163023303266715591396745357472463341356969319556862236385556442904650009255138279232984377682804793224148996875324569988553808409865099882747099149617352970774643108291836908871124753511856578160489828404865664010 +570689556 1980693879 108353275894436996626884805554770441694866167783124178905252902978286824751598925059178987939656961896173921225105217325495780672752694383439806863122466053616930970271706866769895033633709670957150865005763659847698663978549871624628298476651867451354816053985969930735100533712902146229305011837945607699037 +2103057616 691067770 27024056452709622585420653808400360576905666723601175215091499609471301967744143329187436673102391151329548614036436716051750524604202541651425479133617399916946398092715923340837394931898418514658066714568415577105694330058750941172815095999450748361179045856199026312487393802486505084466623313733605784416 +481705774 1641440342 117722260864906582664053412535574009960206959347375143271559843536103545468155917636456429488071536410856812908716077868452921005581676036410474437330482920141777150620686622782118823530416466223519936589968714322291361670902315520017103387742935706013660879451297004924070011539277017717095949755265539759012 diff --git a/Tests/Auth/OpenID/data/dhpriv b/Tests/Auth/OpenID/data/dhpriv new file mode 100644 index 0000000..0fa5231 --- /dev/null +++ b/Tests/Auth/OpenID/data/dhpriv @@ -0,0 +1,29 @@ +130706940119084053627151828062879423433929180135817317038378606310097533503449582079984816816837125851552273641820339909167103200910805078308128174143174269944095368580519322913514764528012639683546377014716235962867583443566164615728897857285824741767070432119909660645255499710701356135207437699643611094585 139808169914464096465921128085565621767096724855516655439365028496569658038844954238931647642811548254956660405394116677296461848124300258439895306367561416289126854788101396379292925819850897858045772500578222021901631436550118958972312221974009238050517034542286574826081826542722270952769078386418682059418 +91966407878983240112417790733941098492087186469785726449910011271065622315680646030230288265496017310433513856308693810812043160919214636748486185212617634222158204354206411031403206076739932806412551605172319515223573351072757800448643935018534945933808900467686115619932664888581913179496050117713298715475 88086484332488517006277516020842172054013692832175783214603951240851750819999098631851571207693874357651112736088114133607400684776234181681933311972926752846692615822043533641407510569745606256772455614745111122033229877596984718963046218854103292937700694160593653595134512369959987897086639788909618660591 +94633950701209990078055218830969910271587805983595045023718108184189787131629772007048606080263109446462048743696369276578815611098215686598630889831104860221067872883514840819381234786050098278403321905311637820524177879167250981289318356078312300538871435101338967079907049912435983871847334104247675360099 136836393035803488129856151345450008294260680733328546556640578838845312279198933806383329293483852515700876505956362639881210101974254765087350842271260064592406308509078284840473735904755203614987286456952991025347168970462354352741159076541157478949094536405618626397435745496863324654768971213730622037771 +24685127248019769965088146297942173464487677364928435784091685260262292485380918213538979925891771204729738138857126454465630594391449913947358655368215901119137728648638547728497517587701248406019427282237279437409508871300675355166059811431191200555457304463617727969228965042729205402243355816702436970430 103488011917988946858248200111251786178288940265978921633592888293430082248387786443813155999158786903216094876295371112716734481877806417714913656921169196196571699893360825510307056269738593971532017994987406325068886420548597161498019372380511676314312298122272401348856314619382867707981701472607230523868 +116791045850880292989786005885944774698035781824784400772676299590038746153860847252706167458966356897309533614849402276819438194497464696186624618374179812548893947178936305721131565012344462048549467883494038577857638815386798694225798517783768606048713198211730870155881426709644960689953998714045816205549 25767875422998856261320430397505398614439586659207416236135894343577952114994718158163212134503751463610021489053571733974769536157057815413209619147486931502025658987681202196476489081257777148377685478756033509708349637895740799542063593586769082830323796978935454479273531157121440998804334199442003857410 +75582226959658406842894734694860761896800153014775231713388264961517169436476322183886891849966756849783437334069692683523296295601533803799559985845105706728538458624387103621364117548643541824878550074680443708148686601108223917493525070861593238005735446708555769966855130921562955491250908613793521520082 51100990616369611694975829054222013346248289055987940844427061856603230021472379888102172458517294080775792439385531234808129302064303666640376750139242970123503857186428797403843206765926798353022284672682073397573130625177187185114726049347844460311761033584101482859992951420083621362870301150543916815123 +22852401165908224137274273646590366934616265607879280260563022941455466297431255072303172649495519837876946233272420969249841381161312477263365567831938496555136366981954001163034914812189448922853839616662859772087929140818377228980710884492996109434435597500854043325062122184466315338260530734979159890875 35017410720028595029711778101507729481023945551700945988329114663345341120595162378885287946069695772429641825579528116641336456773227542256911497084242947904528367986325800537695079726856460817606404224094336361853766354225558025931211551975334149258299477750615397616908655079967952372222383056221992235704 +37364490883518159794654045194678325635036705086417851509136183713863262621334636905291385255662750747808690129471989906644041585863034419130023070856805511017402434123099100618568335168939301014148587149578150068910141065808373976114927339040964292334109797421173369274978107389084873550233108940239410902552 40916262212189137562350357241447034318002130016858244002788189310078477605649010031339865625243230798681216437501833540185827501244378529230150467789369234869122179247196276164931090039290879808162629109742198951942358028123056268054775108592325500609335947248599688175189333996086475013450537086042387719925 +42030470670714872936404499074069849778147578537708230270030877866700844337372497704027708080369726758812896818567830863540507961487472657570488625639077418109017434494794778542739932765561706796300920251933107517954265066804108669800167526425723377411855061131982689717887180411017924173629124764378241885274 124652439272864857598747946875599560379786580730218192165733924418687522301721706620565030507816884907589477351553268146177293719586287258662025940181301472851649975563004543250656807255226609296537922304346339513054316391667044301386950180277940536542183725690479451746977789001659540839582630251935163344393 +33176766914206542084736303652243484580303865879984981189372762326078776390896986743451688462101732968104375838228070296418541745483112261133079756514082093269959937647525005374035326747696591842313517634077723301677759648869372517403529488493581781546743147639937580084065663597330159470577639629864369972900 67485835091897238609131069363014775606263390149204621594445803179810038685760826651889895397414961195533694176706808504447269558421955735607423135937153901140512527504198912146656610630396284977496295289999655140295415981288181545277299615922576281262872097567020980675200178329219970170480653040350512964539 +131497983897702298481056962402569646971797912524360547236788650961059980711719600424210346263081838703940277066368168874781981151411096949736205282734026497995296147418292226818536168555712128736975034272678008697869326747592750850184857659420541708058277866000692785617873742438060271311159568468507825422571 5400380840349873337222394910303409203226429752629134721503171858543984393161548520471799318518954232197106728096866840965784563043721652790856860155702760027304915133166173298206604451826182024471262142046935060360564569939062438160049193241369468208458085699995573492688298015026628427440418009025072261296 +83265103005695640943261961853521077357830295830250157593141844209296716788437615940096402365505416686459260302419338241462783388722843946886845478224048360927114533590583464979009731440049610985062455108831881153988321298531365779084012803908832525921630534096740755274371500276660832724874701671184539131864 141285570207910287798371174771658911045525474449663877845558585668334618068814605961306961485855329182957174312715910923324965889174835444049526313968571611940626279733302104955951067959291852710640374412577070764165811275030632465290729619533330733368808295932659463215921521905553936914975786500018720073003 +68435028583616495789148116911096163791710022987677894923742899873596891423986951658100606742052014161171185231735413902875605720814417622409817842932759492013585936536452615480700628719795872201528559780249210820284350401473564919576289210869896327937002173624497942136329576506818749730506884927872345019446 134655528287263100540003157571441260698452262106680191153945271167894435782028803135774578949200580551016388918860856991026082917835209212892423567114480975540305860034439015788120390011692862968771136814777768281366591257663821495720134621172848947971117885754539770645621669309650476331439675400544167728223 +97765390064836080322590528352647421920257073063706996347334558390461274981996865736612531330863478931481491964338380362350271734683183807511097331539820133036984271653285063355715726806139083282458695728902452215405696318402583540317419929113959816258829534543044153959951908676300847164682178008704099351835 92552521881196975294401505656851872247567784546370503402756239533783651371688190302773864319828182042605239246779598629409815474038541272600580320815319709309111399294952620375093803971373108792300726524826209329889463854451846561437729676142864421966497641824498079067929811613947148353921163336822026640804 +145767094672933012300753301037546647564595762930138884463767054235112032706630891961371504668013023047595721138624016493638510710257541241706724342585654715468628355455898091951826598092812212209834746162089753649871544789379424903025374228231365026585872808685759231756517703720396301355299998059523896918448 116669462839999965355861187716880953863237226719689755457884414384663576662696981997535568446560375442532084973721539944428004043491468494548231348032618218312515409944970197902589794303562379864012797605284844016184274353252071642511293089390472576498394410829972525726474727579603392265177009323768966538608 +34172517877854802711907683049441723730724885305592620486269966708379625109832852005775048584124451699198484092407720344962116726808090368739361658889584507734617844212547181476646725256303630128954338675520938806905779837227983648887192531356390902975904503218654196581612781227843742951241442641220856414232 126013077261793777773236390821108423367648447987653714614732477073177878509574051196587476846560696305938891953527959347566502332765820074506907037627115954790645652211088723122982633069089920979477728376746424256704724173255656757918995039125823421607024407307091796807227896314403153380323770001854211384322 +9979624731056222925878866378063961280844793874828281622845276060532093809300121084179730782833657205171434732875093693074415298975346410131191865198158876447591891117577190438695367929923494177555818480377241891190442070100052523008290671797937772993634966511431668500154258765510857129203107386972819651767 76559085024395996164590986654274454741199399364851956129137304209855150918182685643729981600389513229011956888957763987167398150792454613751473654448162776379362213885827651020309844507723069713820393068520302223477225569348080362344052033711960892643036147232270133731530049660264526964146237693063093765111 +18162696663677410793062235946366423954875282212790518677684260521370996677183041664345920941714064628111537529793170736292618705900247450994864220481135611781148410617609559050220262121494712903009168783279356915189941268264177631458029177102542745167475619936272581126346266816618866806564180995726437177435 63244550218824945129624987597134280916829928261688093445040235408899092619821698537312158783367974202557699994650667088974727356690181336666077506063310290098995215324552449858513870629176838494348632073938023916155113126203791709810160925798130199717340478393420816876665127594623142175853115698049952126277 +4817943161362708117912118300716778687157593557807116683477307391846133734701449509121209661982298574607233039490570567781316652698287671086985501523197566560479906850423709894582834963398034434055472063156147829131181965140631257939036683622084290629927807369457311894970308590034407761706800045378158588657 61612160237840981966750225147965256022861527286827877531373888434780789812764688703260066154973576040405676432586962624922734102370509771313805122788566405984830112657060375568510809122230960988304085950306616401218206390412815884549481965750553137717475620505076144744211331973240555181377832337912951699135 +36363324947629373144612372870171042343590861026293829791335153646774927623889458346817049419803031378037141773848560341251355283891019532059644644509836766167835557471311319194033709837770615526356168418160386395260066262292757953919140150454538786106958252854181965875293629955562111756775391296856504912587 86831561031659073326747216166881733513938228972332631084118628692228329095617884068498116676787029033973607066377816508795286358748076949738854520048303930186595481606562375516134920902325649683618195251332651685732712539073110524182134321873838204219194459231650917098791250048469346563303077080880339797744 +26406869969418301728540993821409753036653370247174689204659006239823766914991146853283367848649039747728229875444327879875275718711878211919734397349994000106499628652960403076186651083084423734034070082770589453774926850920776427074440483233447839259180467805375782600203654373428926653730090468535611335253 100139935381469543084506312717977196291289016554846164338908226931204624582010530255955411615528804421371905642197394534614355186795223905217732992497673429554618838376065777445760355552020655667172127543653684405493978325270279321013143828897100500212200358450649158287605846102419527584313353072518101626851 +92613116984760565837109105383781193800503303131143575169488835702472221039082994091847595094556327985517286288659598094631489552181233202387028607421487026032402972597880028640156629614572656967808446397456622178472130864873587747608262139844319805074476178618930354824943672367046477408898479503054125369731 30023391082615178562263328892343821010986429338255434046051061316154579824472412477397496718186615690433045030046315908170615910505869972621853946234911296439134838951047107272129711854649412919542407760508235711897489847951451200722151978578883748353566191421685659370090024401368356823252748749449302536931 +31485815361342085113278193504381994806529237123359718043079410511224607873725611862217941085749929342777366642477711445011074784469367917758629403998067347054115844421430072631339788256386509261291675080191633908849638316409182455648806133048549359800886124554879661473112614246869101243501787363247762961784 114503770698890543429251666713050844656853278831559195214556474458830029271801818536133531843456707474500106283648085144619097572354066554819887152106174400667929098257361286338795493838820850475790977445807435511982704395422526800272723708548541616513134676140304653112325071112865020365664833601046215694089 +76882090884790547431641385530818076533805072109483843307806375918023300052767710853172670987385376253156912268523505310624133905633437815297307463917718596711590885553760690350221265675690787249135345226947453988081566088302642706234126002514517416493192624887800567412565527886687096028028124049522890448168 15056463217273240496622619354104573042767532856243223052125822509781815362480522535564283485059790932505429110157271454207173426525345813426696743168079246510944969446574354255284952839036431873039487144279164893710061580467579842173706653409487110282515691099753380094215805485573768509475850463001549608836 +52345178981230648108672997265819959243255047568833938156267924185186047373470984278294897653277996726416846430969793375429223610099546622112048283560483136389901514170116723365811871938630317974150540909650396429631704968748113009366339718498979597226137532343384889080245796447593572468846438769413505393967 32148494517199936472358017244372701214529606506776255341152991328091526865643069587953759877295255050519124541457805199596762210567333445908166076384465183589342153762720515477404466193879418014196727238972417616122646440870364200208488239778452378059236162633837824948613596114768455832408342040970780086 +41095268619128788015767564971105114602454449306041732792746397800275041704886345704294273937217484580365505320134717320083763349380629342859670693445658118959823430378844830923452105707338162448974869312012791385772125813291388247857971218575518319578818336960572244046567099555399203328678654466958536663208 92166550199033418923713824997841892577149715275633481076285269142670107687867024550593869464613175882141630640739938334001211714884975032600306279287443909448541179109981755796752132502127330056736913454039526413284519137059580845856736918773597087836203497066909257930043736166431682872083389105176299181629 +40049143661018504441607875135884755310012910557581028447435354354754245291878800571089144452035026644953322330676651798951447670184106450649737772686119714700743396359069052813433030118630105307022867200053964644574786137276428546712005171080129190959914708907200288299169344380390093918556722227705114244981 108159089972386282154772900619022507336076619354549601813179459338897131937353741544606392560724999980281424266891537298473163753022749859939445293926707568015958367188089915420630082556748668489756475027008449860889202622698060097015044886961901650857610841562477736791450080980702347705778074391774667412741 +69905259478181995876884927656894491893594530150260951315109404530530357998889589977208787140430938039028941393673520799460431992051993157468616168400324834880926190141581037597526917869362292931957289043707855837933490285814769110495657056206391880865972389421774822461752702336812585852278453803972600333734 71821415380277072313878763768684432371552628204186742842154591000123020597011744840460964835414360968627162765288463383113375595799297552681618876474019263288277398833725479226930770694271622605114061622753165584075733358178384410640349907375170170910499615355511313349300918885560131539570707695789106185664 +26945345439378873515011714350080059082081595419023056538696949766471272811362104837806324694947413603019863785876836706911406330379274553386254346050697348395574746891556054334903838949157798006141473389066020212044825140294048709654273698482867946522782450500680195477050110145664069582549935651920545151500 80313315938584480048642653013876614091607852535582224914294013785054094052454758327935781971746329853786568549510067442145637007308960551652864942042189241081946607011847245280773379099020221884296226818685556430275385068764313042226925852500883894269809033380734632866477789520106865758504064806906234130588 diff --git a/Tests/Auth/OpenID/data/hmac-sha1.txt b/Tests/Auth/OpenID/data/hmac-sha1.txt new file mode 100644 index 0000000..4299a96 --- /dev/null +++ b/Tests/Auth/OpenID/data/hmac-sha1.txt @@ -0,0 +1,49 @@ +test_case = 1 +key = 0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b +key_len = 20 +data = "Hi There" +data_len = 8 +digest = 0xb617318655057264e28bc0b6fb378c8ef146be00 + +test_case = 2 +key = "Jefe" +key_len = 4 +data = "what do ya want for nothing?" +data_len = 28 +digest = 0xeffcdf6ae5eb2fa2d27416d5f184df9c259a7c79 + +test_case = 3 +key = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +key_len = 20 +data = 0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd +data_len = 50 +digest = 0x125d7342b9ac11cd91a39af48aa17b4f63f175d3 + +test_case = 4 +key = 0x0102030405060708090a0b0c0d0e0f10111213141516171819 +key_len = 25 +data = 0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd +data_len = 50 +digest = 0x4c9007f4026250c6bc8414f9bf50c86c2d7235da + +test_case = 5 +key = 0x0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c +key_len = 20 +data = "Test With Truncation" +data_len = 20 +digest = 0x4c1a03424b55e07fe7f27be1d58bb9324a9a5a04 +digest-96 = 0x4c1a03424b55e07fe7f27be1 + +test_case = 6 +key = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +key_len = 80 +data = "Test Using Larger Than Block-Size Key - Hash Key First" +data_len = 54 +digest = 0xaa4ae5e15272d00e95705637ce8a3b55ed402112 + +test_case = 7 +key = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +key_len = 80 +data = "Test Using Larger Than Block-Size Key and Larger Than One Block-Size Data" +data_len = 73 +digest = 0xe8e99d0f45237d786d6bbaa7965c7808bbff1a91 diff --git a/Tests/Auth/OpenID/data/hmac-sha256.txt b/Tests/Auth/OpenID/data/hmac-sha256.txt new file mode 100644 index 0000000..a0b2e73 --- /dev/null +++ b/Tests/Auth/OpenID/data/hmac-sha256.txt @@ -0,0 +1,29 @@ +test_case = 1 +key = 0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b +data = 0x4869205468657265 +digest = 0xb0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7 + +test_case = 2 +key = 0x4a656665 +data = 0x7768617420646f2079612077616e7420666f72206e6f7468696e673f +digest = 0x5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843 + +test_case = 3 +key = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +data = 0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd +digest = 0x773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe + +test_case = 4 +key = 0x0102030405060708090a0b0c0d0e0f10111213141516171819 +data = 0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd +digest = 0x82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b + +test_case = 6 +key = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +data = 0x54657374205573696e67204c6172676572205468616e20426c6f636b2d53697a65204b6579202d2048617368204b6579204669727374 +digest = 0x60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54 + +test_case = 7 +key = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +data = 0x5468697320697320612074657374207573696e672061206c6172676572207468616e20626c6f636b2d73697a65206b657920616e642061206c6172676572207468616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565647320746f20626520686173686564206265666f7265206265696e6720757365642062792074686520484d414320616c676f726974686d2e +digest = 0x9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2 diff --git a/Tests/Auth/OpenID/data/linkparse.txt b/Tests/Auth/OpenID/data/linkparse.txt new file mode 100644 index 0000000..eb13e6b --- /dev/null +++ b/Tests/Auth/OpenID/data/linkparse.txt @@ -0,0 +1,594 @@ +Num Tests: 73 + +OpenID link parsing test cases +Copyright (C) 2005-2008, JanRain, Inc. +See COPYING for license information. + +File format +----------- + +All text before the first triple-newline (this chunk) should be ignored. + +This file may be interpreted as Latin-1 or UTF-8. + +Test cases separated by three line separators (`\n\n\n'). The test +cases consist of a headers section followed by a data block. These are +separated by a double newline. The headers consist of the header name, +followed by a colon, a space, the value, and a newline. There must be +one, and only one, `Name' header for a test case. There may be zero or +more link headers. The `Link' header consists of whitespace-separated +attribute pairs. A link header with an empty string as a value +indicates an empty but present link tag. The attribute pairs are `=' +separated and not quoted. + +Optional Links and attributes have a trailing `*'. A compilant +implementation may produce this as output or may not. A compliant +implementation will not produce any output that is absent from this +file. + + +Name: Well-formed link rel (in CAPS) +Link: rel=openid.server href=http://www.myopenid.com/server + +<HTML> + <HEAD> + <LINK REL="openid.server" + HREF="http://www.myopenid.com/server" /> + </HEAD> +</HTML> + + +Name: No link tag at all + +<html> +<head> +</head> +</html> + + +Name: Link element first + +<link> + + +Name: Link inside HTML, not head + +<html> +<link> + + +Name: Link inside head, not html + +<head> +<link> + + +Name: Link inside html, after head + +<html> +<head> +</head> +<link> + + +Name: Link inside html, before head + +<html> +<link> +<head> + + +Name: Link before html and head + +<link> +<html> +<head> + + +Name: Link after html document with head + +<html> +<head> +</head> +</html> +<link> + + +Name: Link inside html inside head, inside another html + +<html> +<head> +<html> +<link> + + +Name: Link inside html inside head + +<head> +<html> +<link> + + +Name: link inside body inside head inside html + +<html> +<head> +<body> +<link> + + +Name: Link inside head inside head inside html + +<html> +<head> +<head> +<link> + + +Name: Link inside script inside head inside html + +<html> +<head> +<script> +<link> +</script> + + +Name: Link inside comment inside head inside html + +<html> +<head/> +<link> + + +Name: Link inside of head after short head + +<html> +<head/> +<head> +<link> + + +Name: Plain vanilla +Link: + +<html> +<head> +<link> + + +Name: Ignore tags in the <script:... > namespace +Link*: + +<html> +<head> +<script:paddypan> +<link> +</script:paddypan> + + +Name: Short link tag +Link: + +<html> +<head> +<link/> + + +Name: Spaces in the HTML tag +Link: + +<html > +<head> +<link> + + +Name: Spaces in the head tag +Link: + +<html> +<head > +<link> + + +Name: Spaces in the link tag +Link: + +<html> +<head> +<link > + + +Name: No whitespace +Link: + +<html><head><link> + + +Name: Closed head tag +Link: + +<html> +<head> +<link> +</head> + + +Name: One good, one bad (after close head) +Link: + +<html> +<head> +<link> +</head> +<link> + + +Name: One good, one bad (after open body) +Link: + +<html> +<head> +<link> +<body> +<link> + + +Name: ill formed (missing close head) +Link: + +<html> +<head> +<link> +</html> + + +Name: Ill formed (no close head, link after </html>) +Link: + +<html> +<head> +<link> +</html> +<link> + + +Name: Ignore random tags inside of html +Link: + +<html> +<delicata> +<head> +<title> +<link> + + +Name: case-folding +Link*: + +<HtMl> +<hEaD> +<LiNk> + + +Name: unexpected tags +Link: + +<butternut> +<html> +<summer> +<head> +<turban> +<link> + + +Name: un-closed script tags +Link*: + +<html> +<head> +<script> +<link> + + +Name: un-closed script tags (no whitespace) +Link*: + +<html><head><script><link> + + +Name: un-closed comment +Link*: + +<html> +<head> +<!-- +<link> + + +Name: un-closed CDATA +Link*: + +<html> +<head> +<![CDATA[ +<link> + + +Name: cdata-like +Link*: + +<html> +<head> +<![ACORN[ +<link> +]]> + + +Name: comment close only +Link: + +<html> +<head> +<link> +--> + + +Name: Vanilla, two links +Link: +Link: + +<html> +<head> +<link> +<link> + + +Name: extra tag, two links +Link: +Link: + +<html> +<gold nugget> +<head> +<link> +<link> + + +Name: case-fold, body ends, two links +Link: +Link*: + +<html> +<head> +<link> +<LiNk> +<body> +<link> + + +Name: simple, non-quoted rel +Link: rel=openid.server + +<html><head><link rel=openid.server> + + +Name: short tag has rel +Link: rel=openid.server + +<html><head><link rel=openid.server/> + + +Name: short tag w/space has rel +Link: rel=openid.server + +<html><head><link rel=openid.server /> + + +Name: extra non-attribute, has rel +Link: rel=openid.server + +<html><head><link hubbard rel=openid.server> + + +Name: non-attr, has rel, short +Link: rel=openid.server + +<html><head><link hubbard rel=openid.server/> + + +Name: non-attr, has rel, short, space +Link: rel=openid.server + +<html><head><link hubbard rel=openid.server /> + + +Name: misplaced slash has rel +Link: rel=openid.server + +<html><head><link / rel=openid.server> + + +Name: quoted rel +Link: rel=openid.server + +<html><head><link rel="openid.server"> + + +Name: single-quoted rel +Link: rel=openid.server + +<html><head><link rel='openid.server'> + + +Name: two links w/ rel +Link: x=y +Link: a=b + +<html><head><link x=y><link a=b> + + +Name: non-entity +Link: x=&y + +<html><head><link x=&y> + + +Name: quoted non-entity +Link: x=&y + +<html><head><link x="&y"> + + +Name: quoted entity +Link: x=& + +<html><head><link x="&"> + + +Name: entity not processed +Link: x= + +<html><head><link x=""> + + +Name: < +Link: x=< + +<html><head><link x="<"> + + +Name: > +Link: x=> + +<html><head><link x=">"> + + +Name: " +Link: x=" + +<html><head><link x="""> + + +Name: &" +Link: x=&" + +<html><head><link x="&""> + + +Name: mixed entity and non-entity +Link: x=&"…> + +<html><head><link x="&"…>"> + + +Name: mixed entity and non-entity (w/normal chars) +Link: x=x&"…>x + +<html><head><link x="x&"…>x"> + + +Name: broken tags +Link*: x=y + +<html><head><link x=y<> + + +Name: missing close pointy +Link: z=y + +<html><head><link x=y<link z=y /> + + +Name: missing attribute value +Link: x=y y*= +Link: x=y + +<html><head><link x=y y=><link x=y /> + + +Name: Missing close pointy (no following) +Link*: x=y + +<html><head><link x=y + + +Name: Should be quoted +Link: x*=< + +<html><head><link x="<"> + + +Name: Should be quoted (2) +Link: x*=> + +<html><head><link x=">"> + + +Name: Repeated attribute +Link: x=y + +<html><head><link x=z x=y> + + +Name: Repeated attribute (2) +Link: x=y + +<html><head><link x=y x=y> + + +Name: Two attributes +Link: x=y y=z + +<html><head><link x=y y=z> + + +Name: Well-formed link rel="openid.server" +Link: rel=openid.server href=http://www.myopenid.com/server + +<html> + <head> + <link rel="openid.server" + href="http://www.myopenid.com/server" /> + </head> +</html> + + +Name: Well-formed link rel="openid.server" and "openid.delegate" +Link: rel=openid.server href=http://www.myopenid.com/server +Link: rel=openid.delegate href=http://example.myopenid.com/ + +<html><head><link rel="openid.server" + href="http://www.myopenid.com/server" /> + <link rel="openid.delegate" href="http://example.myopenid.com/" /> +</head></html> + + +Name: from brian's livejournal page +Link: rel=stylesheet href=http://www.livejournal.com/~serotta/res/319998/stylesheet?1130478711 type=text/css +Link: rel=openid.server href=http://www.livejournal.com/openid/server.bml + +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <link rel="stylesheet" + href="http://www.livejournal.com/~serotta/res/319998/stylesheet?1130478711" + type="text/css" /> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <meta name="foaf:maker" + content="foaf:mbox_sha1sum '12f8abdacb5b1a806711e23249da592c0d316260'" /> + <meta name="robots" content="noindex, nofollow, noarchive" /> + <meta name="googlebot" content="nosnippet" /> + <link rel="openid.server" + href="http://www.livejournal.com/openid/server.bml" /> + <title>Brian</title> + </head> + + +Name: non-ascii (Latin-1 or UTF8) +Link: x=® + +<html><head><link x="®"> + + diff --git a/Tests/Auth/OpenID/data/n2b64 b/Tests/Auth/OpenID/data/n2b64 new file mode 100644 index 0000000..b12a246 --- /dev/null +++ b/Tests/Auth/OpenID/data/n2b64 @@ -0,0 +1,650 @@ +AA== 0 +AQ== 1 +Ag== 2 +Aw== 3 +BA== 4 +BQ== 5 +Bg== 6 +Bw== 7 +CA== 8 +CQ== 9 +Cg== 10 +Cw== 11 +DA== 12 +DQ== 13 +Dg== 14 +Dw== 15 +EA== 16 +EQ== 17 +Eg== 18 +Ew== 19 +FA== 20 +FQ== 21 +Fg== 22 +Fw== 23 +GA== 24 +GQ== 25 +Gg== 26 +Gw== 27 +HA== 28 +HQ== 29 +Hg== 30 +Hw== 31 +IA== 32 +IQ== 33 +Ig== 34 +Iw== 35 +JA== 36 +JQ== 37 +Jg== 38 +Jw== 39 +KA== 40 +KQ== 41 +Kg== 42 +Kw== 43 +LA== 44 +LQ== 45 +Lg== 46 +Lw== 47 +MA== 48 +MQ== 49 +Mg== 50 +Mw== 51 +NA== 52 +NQ== 53 +Ng== 54 +Nw== 55 +OA== 56 +OQ== 57 +Og== 58 +Ow== 59 +PA== 60 +PQ== 61 +Pg== 62 +Pw== 63 +QA== 64 +QQ== 65 +Qg== 66 +Qw== 67 +RA== 68 +RQ== 69 +Rg== 70 +Rw== 71 +SA== 72 +SQ== 73 +Sg== 74 +Sw== 75 +TA== 76 +TQ== 77 +Tg== 78 +Tw== 79 +UA== 80 +UQ== 81 +Ug== 82 +Uw== 83 +VA== 84 +VQ== 85 +Vg== 86 +Vw== 87 +WA== 88 +WQ== 89 +Wg== 90 +Ww== 91 +XA== 92 +XQ== 93 +Xg== 94 +Xw== 95 +YA== 96 +YQ== 97 +Yg== 98 +Yw== 99 +ZA== 100 +ZQ== 101 +Zg== 102 +Zw== 103 +aA== 104 +aQ== 105 +ag== 106 +aw== 107 +bA== 108 +bQ== 109 +bg== 110 +bw== 111 +cA== 112 +cQ== 113 +cg== 114 +cw== 115 +dA== 116 +dQ== 117 +dg== 118 +dw== 119 +eA== 120 +eQ== 121 +eg== 122 +ew== 123 +fA== 124 +fQ== 125 +fg== 126 +fw== 127 +AIA= 128 +AIE= 129 +AII= 130 +AIM= 131 +AIQ= 132 +AIU= 133 +AIY= 134 +AIc= 135 +AIg= 136 +AIk= 137 +AIo= 138 +AIs= 139 +AIw= 140 +AI0= 141 +AI4= 142 +AI8= 143 +AJA= 144 +AJE= 145 +AJI= 146 +AJM= 147 +AJQ= 148 +AJU= 149 +AJY= 150 +AJc= 151 +AJg= 152 +AJk= 153 +AJo= 154 +AJs= 155 +AJw= 156 +AJ0= 157 +AJ4= 158 +AJ8= 159 +AKA= 160 +AKE= 161 +AKI= 162 +AKM= 163 +AKQ= 164 +AKU= 165 +AKY= 166 +AKc= 167 +AKg= 168 +AKk= 169 +AKo= 170 +AKs= 171 +AKw= 172 +AK0= 173 +AK4= 174 +AK8= 175 +ALA= 176 +ALE= 177 +ALI= 178 +ALM= 179 +ALQ= 180 +ALU= 181 +ALY= 182 +ALc= 183 +ALg= 184 +ALk= 185 +ALo= 186 +ALs= 187 +ALw= 188 +AL0= 189 +AL4= 190 +AL8= 191 +AMA= 192 +AME= 193 +AMI= 194 +AMM= 195 +AMQ= 196 +AMU= 197 +AMY= 198 +AMc= 199 +AMg= 200 +AMk= 201 +AMo= 202 +AMs= 203 +AMw= 204 +AM0= 205 +AM4= 206 +AM8= 207 +ANA= 208 +ANE= 209 +ANI= 210 +ANM= 211 +ANQ= 212 +ANU= 213 +ANY= 214 +ANc= 215 +ANg= 216 +ANk= 217 +ANo= 218 +ANs= 219 +ANw= 220 +AN0= 221 +AN4= 222 +AN8= 223 +AOA= 224 +AOE= 225 +AOI= 226 +AOM= 227 +AOQ= 228 +AOU= 229 +AOY= 230 +AOc= 231 +AOg= 232 +AOk= 233 +AOo= 234 +AOs= 235 +AOw= 236 +AO0= 237 +AO4= 238 +AO8= 239 +APA= 240 +APE= 241 +API= 242 +APM= 243 +APQ= 244 +APU= 245 +APY= 246 +APc= 247 +APg= 248 +APk= 249 +APo= 250 +APs= 251 +APw= 252 +AP0= 253 +AP4= 254 +AP8= 255 +AQA= 256 +AQE= 257 +AQI= 258 +AQM= 259 +AQQ= 260 +AQU= 261 +AQY= 262 +AQc= 263 +AQg= 264 +AQk= 265 +AQo= 266 +AQs= 267 +AQw= 268 +AQ0= 269 +AQ4= 270 +AQ8= 271 +ARA= 272 +ARE= 273 +ARI= 274 +ARM= 275 +ARQ= 276 +ARU= 277 +ARY= 278 +ARc= 279 +ARg= 280 +ARk= 281 +ARo= 282 +ARs= 283 +ARw= 284 +AR0= 285 +AR4= 286 +AR8= 287 +ASA= 288 +ASE= 289 +ASI= 290 +ASM= 291 +ASQ= 292 +ASU= 293 +ASY= 294 +ASc= 295 +ASg= 296 +ASk= 297 +ASo= 298 +ASs= 299 +ASw= 300 +AS0= 301 +AS4= 302 +AS8= 303 +ATA= 304 +ATE= 305 +ATI= 306 +ATM= 307 +ATQ= 308 +ATU= 309 +ATY= 310 +ATc= 311 +ATg= 312 +ATk= 313 +ATo= 314 +ATs= 315 +ATw= 316 +AT0= 317 +AT4= 318 +AT8= 319 +AUA= 320 +AUE= 321 +AUI= 322 +AUM= 323 +AUQ= 324 +AUU= 325 +AUY= 326 +AUc= 327 +AUg= 328 +AUk= 329 +AUo= 330 +AUs= 331 +AUw= 332 +AU0= 333 +AU4= 334 +AU8= 335 +AVA= 336 +AVE= 337 +AVI= 338 +AVM= 339 +AVQ= 340 +AVU= 341 +AVY= 342 +AVc= 343 +AVg= 344 +AVk= 345 +AVo= 346 +AVs= 347 +AVw= 348 +AV0= 349 +AV4= 350 +AV8= 351 +AWA= 352 +AWE= 353 +AWI= 354 +AWM= 355 +AWQ= 356 +AWU= 357 +AWY= 358 +AWc= 359 +AWg= 360 +AWk= 361 +AWo= 362 +AWs= 363 +AWw= 364 +AW0= 365 +AW4= 366 +AW8= 367 +AXA= 368 +AXE= 369 +AXI= 370 +AXM= 371 +AXQ= 372 +AXU= 373 +AXY= 374 +AXc= 375 +AXg= 376 +AXk= 377 +AXo= 378 +AXs= 379 +AXw= 380 +AX0= 381 +AX4= 382 +AX8= 383 +AYA= 384 +AYE= 385 +AYI= 386 +AYM= 387 +AYQ= 388 +AYU= 389 +AYY= 390 +AYc= 391 +AYg= 392 +AYk= 393 +AYo= 394 +AYs= 395 +AYw= 396 +AY0= 397 +AY4= 398 +AY8= 399 +AZA= 400 +AZE= 401 +AZI= 402 +AZM= 403 +AZQ= 404 +AZU= 405 +AZY= 406 +AZc= 407 +AZg= 408 +AZk= 409 +AZo= 410 +AZs= 411 +AZw= 412 +AZ0= 413 +AZ4= 414 +AZ8= 415 +AaA= 416 +AaE= 417 +AaI= 418 +AaM= 419 +AaQ= 420 +AaU= 421 +AaY= 422 +Aac= 423 +Aag= 424 +Aak= 425 +Aao= 426 +Aas= 427 +Aaw= 428 +Aa0= 429 +Aa4= 430 +Aa8= 431 +AbA= 432 +AbE= 433 +AbI= 434 +AbM= 435 +AbQ= 436 +AbU= 437 +AbY= 438 +Abc= 439 +Abg= 440 +Abk= 441 +Abo= 442 +Abs= 443 +Abw= 444 +Ab0= 445 +Ab4= 446 +Ab8= 447 +AcA= 448 +AcE= 449 +AcI= 450 +AcM= 451 +AcQ= 452 +AcU= 453 +AcY= 454 +Acc= 455 +Acg= 456 +Ack= 457 +Aco= 458 +Acs= 459 +Acw= 460 +Ac0= 461 +Ac4= 462 +Ac8= 463 +AdA= 464 +AdE= 465 +AdI= 466 +AdM= 467 +AdQ= 468 +AdU= 469 +AdY= 470 +Adc= 471 +Adg= 472 +Adk= 473 +Ado= 474 +Ads= 475 +Adw= 476 +Ad0= 477 +Ad4= 478 +Ad8= 479 +AeA= 480 +AeE= 481 +AeI= 482 +AeM= 483 +AeQ= 484 +AeU= 485 +AeY= 486 +Aec= 487 +Aeg= 488 +Aek= 489 +Aeo= 490 +Aes= 491 +Aew= 492 +Ae0= 493 +Ae4= 494 +Ae8= 495 +AfA= 496 +AfE= 497 +AfI= 498 +AfM= 499 +AfQ= 500 +AfU= 501 +AfY= 502 +Afc= 503 +Afg= 504 +Afk= 505 +Afo= 506 +Afs= 507 +Afw= 508 +Af0= 509 +Af4= 510 +Af8= 511 +AgA= 512 +ALDs7paJl5xPh6ORH61iDA6pONpV0rTjGiTkLEW2JsVsRKaRiS4AGn2PTR1UZXP0vXAmRXwdSegQgWPUp3Hm3RofRcDh1SykZBLif7ulau1hVO+rhwRyKc7F8F+7LcMf/v+s73eOXUDbbI2r52wfr7skZy/IELhsC8EK6HzhACI3 124241322153253947064453752054205174382289463089695815605736438952932114700118408072544073767229325045596832952652232288773280299665950768731398747700657715829631597019676014848183966683866396215048196276450953653433516126074463193382764063985175903718735372053536664711482497859539116009770850968340298474039 +AOzgU1s6Pd2IkrJlvGND8legXTe50nyDCocI5mwT9rW0YsisY5jaaEOcu51BAq9MmXBPeVX0k/jlXwH4Pn3mCpUAU1rEOsTdcmSJp35siKliDdhTZHHdZNMW+igfXGX5OCsA/BaBcGnE6NnrGWXKyTOoVUGQLEkL2T5yhNUaCT83 166340174936369324883416612727439279977041963320514134445183426741643586944819834936989524033374309932122967866930503619179389342537723598234062828695747850043368572301869699886931403612266216965783079972698791813140295203826980649434652168563255385527187360027803388963151668338040517316899628026707657178935 +AO8hrpw+lDiJ13JahLtCb1RenupQcNd0wlTSck9OLL8wB/x6gAoj0PTLV05eZIbz43N3GUSDmmckjlxdHXiBJ9rsoB0P95l1CWIV+4rXblCqxmOdmlm6VZ13bqbI0x7l0cjeMrkmk+yJ067WqUolqQBlUWMTuJVfkxALJYH5xr/C 167923899524385316022824282304301434707626789716026029252173742527362300338760906999615029022863637963070711762128687835779073122264515776657475985362344360699359591353388569856862973447791264902182048648600267737826849280828116753682917256540180401899752566540869918949003470368970029744573140084219550547906 +QxAn7yrdVs5tlHV+Glbqdaj67c6Ni8am3bBLOL8PV5HbdrLf2xIPmNugo6MfUwFSnT+ZPJ51+VTOsItaNwCFju0Eh1cqyP3JWyLRPE7emKuo6xRhf+5ik0pTg77LEF4JXW6ofDqirpR4alFi0G2d9yImQPphsYJwYGF/nNT8u0Q= 47093316905427544098193936500644355852669366083115552072584429220248776817916430034648347490325490701471113667554329499736495877969341478442613611948220957798780043076906836236556612316544460763366275536846463456405604189392790111985912854476264292503164100482712281088955640964034295834935468665872932715332 +AI9PVzrbJUvmCihwSFans1lBKwudGEZpWWu8pkSK2zVgzGhMvUoGgMp6TG2zsUd1tV8zv7KsVD2t6pXmjT1wPUynufq97GVHI06SGpflDTt30WboYRh3DgYxvso1sOjUXpnDezcaqc2Aiz4nV5MSShkBlyBjA8z2worHDE+uXqw0 100635651531872121827765663065728398779771663753008344681972226973080394359405041113312675686974926993279775427390065833081040771269307007695807025882757371805607979134114890454059957194316765342461291139168706134406917264848659448693866813989352429841300235734400772946895458374870482441457514575059390213172 +FiinVicXOqqRLpxcGTorQpSAGeQ/PfDOuzYK9ViFtmPv6D0cYPfhUH4qXEHOejvmX+0b4lfaX8MWPVZxlqpfXiU9BhG76HJxkLF4ysipukeOvhoHzvcxE5bnhSF1i//bOSifATBLBEZInwqSVg5tHHPuuCkwTL91NqhOulp7Lsk= 15560440884463435471963622630292643727112462888414585143143739400703889930416938984547754943252935620248108237258540176511252143752416771350868493435174026287082706690332705481726295797196444796135827460509780634261726494455068460028424141500629527968240913757449787164107068039175831847071025316475940056777 +aYrxyQN/hkBne2ayqo2/iDLF3DZGgk080SOMJfsj9h3Z1OfFZM7TJA+y+/O7niqatosvKrfHrAw+Qs7c6tCZ6NPwYJ4QJLOF9bqH2u2a3fkI954voNUctlUagYUJsZXV8hdhLM6NwUyIZ3ZFkPcpTZp7nKQQ84tr1a8VjDIT5/o= 74114640794666001532816944350975062126079079113921109750255283189037502412929005615388097912507598112836936032143435813588205939470002911374442844578739574773399427907766548612582213272643279263782396527705126350063372192910060171635870872236876399794128383338399728947176692692942605589343038282957050865658 +AMpCUeKUX/vtRslWiUUuXNl1KA9uDAWjMUkTrdsxxRDESI7iZIn3TR9lW+0kV5fzkLF18iYLAwSGBmX1PS/T0UVFmoBPJ9yS7yktNL0lpQ3noyGFn8HHZ6XB3FkH3jegIfGbvwwhnhhFzpHPrXlpO5iU5Y+rexzp2XHWt4yJGuIL 142031143422642739313498629438991149460874309300342349421794421544918823888598660275343727563280565210534243383322796489809683834300630555650331646026843796764549231159336347965502383849513994449309613369541991287590422095953275586374371960367000083487965487661436037637475372929033613295072397262739084075531 +AIMIQVz0JIEKEI+PREu94m3v9XoiU/Q0CpsSuqkwSSje+Wyul5ea9oU5qgtOpdkMUOW91BJo0DW/GMZ8v3C4qyyP29TtjCcAHObJi9hfLSlnTSuzXZnDStooYYKqzfToLToCaAJKCXiXAVW0vWtapLnyqafrf/KgyGZ5u4HfXKY0 92013973253053602863003242446596060337454881568126916916519869242232429836082762281129448384605359749247852792606718908482332975424967542242332487707042773885428473061056052851768940900752317020681189773407893371297668591494665352294885305475871917069040377145530889271334616499701769138948975263435137525300 +ANfP+zPBTR27afneyac1KJhOB5Pq3AXB+SoAXJvQI/GkSoNhw5KdfqoIkLcoJi8wClCm424Gm1AdrdGwDFOM/iKTSPkrvMag93+b2EbQGX66/n2X3YRFNkgq/Gtb+2M8oCcAL054Z/iiMD67aU5RWzjqS64ePHsn01bJ7dqLgpMO 151548639867177154896951257541227014781655576169318283047778755573323724856619156348444192550664853912434681577093459933599575436686424046466113215132845213008587152894642577278656978304699131916299275797578171518984206145555369576872231567191579337901913492071976578289189524123204040497290426960375042970382 +AK0kHtacLGu1NFWMADq2rG8hpzM4UEYyPOL+aMJbnwXcUYptRIxb0YFZg35RN/RiZs4lQsiq+kEJKzMMV71TsJq59vMkIZhZoB3t8g9ZqBZuq0JYcTICDwRpNSttJidVpJ6P9sR3s1xPMYKdlSwt6EEc9htOXfZU+yHKYgn98X60 121583812047864398969816595368193171848971298823388059338224714026742264861090347096116404814514279627148994345584790617974476594451626305761040465570524035369799925437276511604752129817947910677564301623631349399504187314174538914591944778074509068973226322566160587813128746039859381466427380402262866230964 +W3sZlWW1Aev3x/DiH9MzwCAZzBj++x9cknLfGAHwgFqkLH6vimEH/r8hi85hzlCOG5CjwhoZ0D/Hsfr26ZJ3X4chG84byrfDnek1V9mm1++v+clJvlYgcuVgn2Opsba2TILTm1MDB+Ujs9brJ2AAKrE9+ep5nvtQVeG9PUGtdlo= 64240043913835461386212515483198059541440539167395859777194837833769712010594411295323900074066077107346806786205590345517755715510695858065925747020336398305793661773798243627926904542715123849691490667964262778804487343218972081260210371192903128886030021862362141928329650003493687310970684093289133340250 +FTQRk9/BIj21gbLwI22fHJWYj+8Ghdcc613hOtJ+/hQmh73HwTXLpaGK9aCptxVbpjW0r/bxaRjmgxu9u1CCZh5yRd7Z46Wk/LIPXGd3ycQzqRMFB7TISFQGJIcFoxRp3Eb5wa2OyrUg7c/D+kb7oFJq9P7mEwIh8TpLzwmu4SU= 14889529068556301710329043521845510156960298822469914567758538023025100741826628180855835334285179977296740667353391766487166458692144569279381035582718738461626140662441222061900764829681913534146898551570916312642104487829660946024590782808750587095559047648957238487820069966851521487428624726655438348581 +APYXO6uGvs9qWiEAkcWsaCaCrGJJCP2Z1g++XlJ67oZIgEoWITn3T/R2/c4edAfwUUzNHAYZN1h2dSrRoqlrRXrbxFtGOuRCUrXcGLFFcEbTrtm+z5z8xGRfcorx7Cu3FIBPMK5XcGPcbRZdyP1gBkeDMvuBNAo0/To+LP/dhCNM 172810804474418448604443090732221483571611665465870520701624598983692130272337358406272727413570938793741430131635927237320173996217984860203754686741782921346604605228620148450611724714868551781003004492587584071978757421616871762681049508123223983431502852926521520561941051298696758046005573332373854233420 +AIDNxhnDEe1kTJ3XGfTS8zKXeXPRdw5yifm8j8Ibzj/quExy7hFPtKct8hRskPR2qwTlMiW9Ra8Npg2USsqHV0rBoIkX7E3psxq5LBfp/q00l3SEBuLL4K2FGR87bPgU+Duk3RVrNMnColiTcnAR5XkoeWhn/r9MfJMIN9Y0FEh8 90449107125498302548188660544012777357148118984122992664008792590422284061463729084479315745509706793674355738023180454297730948397413371686013210006834869294564190666543874617716180411178090109573192518129248278410216362657350215009192850017507998797754539132540293137589672869131300859207213449571846080636 +AIRWavxYRsGlH0Yr0DudwrpYtbrByf9ZsDawKom7ubiRfepqYzcBlwt4adMMnkYSaXeYtOsD4KBm2ZvLKN3++RkYNmxgkyarORBEg7ERyiThBj7Ksw57pNHCAoHtBEhH7Wp9mHhuZtPvPgCEptmwCu9rYhLt4zZp+Zq8a02dkXvM 92930601962515884925250459851491509622611227724602941760145671636277317511265759558869239180653492283311584982044597979173761619470326096725838197524704577188104121460089235709339932110663536557497112887112782062772810759971739760085128369628777812332518137107605855679096146402427144185104230596200130247628 +AMNJGLcAiJtL5fUfkesWKYJurdYSnvsOZeZcrg7bemkEVjF6S9CcojimUl+ncr/YY5/EXnU0mg84fObtDxWWdJ7z7l0CFcoALTyEatDYKshT0xvdKY3u+LUShxIAyk8EcGnf+KoEaa4Mx3tg2oTBnVegXClOakNTWw8bu2ItagoQ 137134165107366719462230252606689766470445826753581409513106273517221906418464863733870948759313279128624638614534848890858250894834883265387344539280755177217350585564186248554307335197387734431939154077778003706720017441895613190141376534460438929588407764609772857975000507660651583780079804513519571438096 +BmGPZt8XqqI1PuLN4K1/PZMi2rfOYtHEMrcwZdSjKRm5qTkd0Pbb/5zPV07TnM0uLRvIQYTLloEY+GYyn0K5gDTEZpEtQ8ee6Y87zYGDwcf20eqYNxkA7FVV71vqCP/Uw3Oi6B+hMvsWZbvv2vH6MkAeADCrezOtwqVS+irftyc= 4480956865245875120472829476982311611308898564405318773810939350829150182630548948231116574193987272498161864310429976564278532538229396846813874244969927890037756969704618336242255039858182439641759659872128285423988638335967412040624105824571426792562334458751137508116412821914961236269913776304372561703 +APqFgCIYbJWuRyEGuOStPvcprj2PILQ0JpgwQ2jLKn3DvkWSd83qh7PWGKozGavsjh803K+ZzI7P2wP+Nc0r0El3q4nzaHvKaCtVRyMwbXv9wYLFZICeM6J1l9ljUMts4tbDoPzkIy3ScU7pYxarBWqMkcBU8qL6NN1vEdkeu0fW 175922170410080716883576123079908758276229469783745771772401183721225804343343063277676406040455068452258961299511343441961963941297631097736305638850193978800615558067791016294285848963023036905095022181004058235239390870177623185946205281141386416867569004073524130001309977475780893497185890756991672600534 +APA/rCcGeH6A+6ZwaBBDM6mB6tTD8mjkrOWEo/pK3MCZ+rrErMBnFp2S19GhlLOfuY8BHS+D834Fdm8+3wKYkWnXZpGb+e3v8ofOQ34G1HvzULOYtrEiC4ISZRt2SSyz2hU+PBXjVnplsHWTRxZDmBxTJdgli4ItAqxGCxj/aJ9m 168708388929747822981923386197903561880341990893945097067702518857172133291360611402092714329372304718329568897960770488377524912057166920574319430820488930520807742026377043178502591886293565177404635365772829346030773275726024973460121300339258054215286249329967181244588558220467488638468686270735376228198 +AKmwrLP108dCGWOWxE/6woJVLRi/Kra/DvdsPkkrZQmWIlUT7IvwM4gU6bUr4f6wpT08cIQls2cGh7dbSEaO0xLa3mmtKhPiAlzSnz0wuifF3JT9U3uXgUfCZuFtE0z7Oi7WTOrpl3k3GA7JFvXnY0lwblIQALVf6oWyNETnajGl 119160465301384937485959146028591622947513292915838943629387700439301197965652871741710280647524383590817798553034250156068573474278225305190573334054718387045488098320076877626430189054572361967283632592181431701411266656256255758079114072932140551282607247364388070762970060420036793573956057551235306893733 +VTe3rCzAL1Sljo3QAXEkAdBy1ZARHZwtrj6ZNRa5ttqd6/l21g4z3iHCeGo4rnE2F8wYTy+NlugjXw86OS+XojW5y6UzTtx0HX5IJ4POqN64aXWmaklGzroBEYWeuFFKcgQN3NOxkuJoDQ6VElP7Epz69kj5CsKJUwL0SjbNrFY= 59841866347633473702601462509811342285929528424012250265905695635971518533504187799047710303717472950129869674786231155102509311322791323986824635569605105662070745033595366004805920086888891275288347907772640070278731650628917037915863439204501060041944275512863990729926528905725569467329169134226609384534 +AIZt1xGhC/HrvpPASsvVIVdsu//tn0noyJmVYh3FdQ6yIh1uce47iCsrV1yvYqx5ZTbC0vnfnbjFcWqH+HtLX/DelgvhEwzqJ8hwQrfE1ShLG4ZjAVo1Z4GCjrDcEUMlwKcunuSJssuxeQuXwTLS92+q6QeBSS7OmfxPX29CLb4B 94399298271083745508290936113986978382457275531684761701599029877008571741877683365769553170771833981099580359640421358853566501815723434822307977440496208486103754978934472597505865596938563438311337045817621762649604204720249750058676095769230214181772215323235427976398686727606000594646472236822594174465 +NIhTPpWXS82VTA0LTd6TfM+HgLgUcmvnMYtLqPpuqCKZwalAycwl0XFYNyVvaY21J94j92ts/lRYgVtHDhk7/9nLXq5js/lsUnG8rWPHJo11JTxvW+df88aX0pw8u+biOWt87vc1MW1dsMTTsJFJAeBx77qU/Cwto95IVqM7vSE= 36889590210230649939994518345793530042252563793069578097360569338647730438860274349862767107939590441616825589851005429465345268710487649366046960918184701290985280638488938340668212498212581853679035928093386035688597446809895381618260692378376844452061580510108168030682664507293277674052032318576713776417 +KXdi4A2Z7tSiiX9YGtDtxUXIfQvPhcc48rUH+Q2SnXL7fLNmr+F4Rf3RiFBRiHKocPfE94pothop5qQJ5X221/DbEKWK6s+ChfQ636jvRxojoLMab3dPtaAPpDJHrfZMxbT4ZaDJT0tpA2e+zZrzBuDs+UUgCpty9nxtdm1gS7A= 29118662951481660380477444121362422614202367719725087486810943918529894738076273660245405874301505615796632229852040910511025841576465052938308369421493312085081188509808322692130449282585522349552501983296872614029139293444558468751646868108213623606366977549477663987815308260383403466635254115908032940976 +AIOTBZQR2EJJRmoWdRNFLG4fceoS3KnRTHRpPdllhHODqdg+QxTOcOvqIzBqgdD0JgO12SuNAjLQOiz0jhd02qkXw9Y1adGuKvL97ARFtNEuJiNzFAj7KpDLy2zk2rPJp4Lp7cjQs0fe8BQYnTzTsNRGm+4ybln/gse1YWu9w8y5 92394618277596007469808288231093678404089765494062813665106014405059399079199990128824492247005602685377185496959522609467906358619318009231448503013528692450191782140091818984176967246749464502089280153086163239846744554575017530385347720563798041108608545014076448155956762636929707905789978331102411214009 +NzfbJRBF4pqEeborJrjoknJgpfK+DZh2k9cE5dcElMPZ2Zn9im7desWGiBSQnu3KbTO4L/t4+m6nFTNcbIJnqbVSMDHdsfG72rG/t89aOuECQw0HMVVgONNNa6i/mw0jZSWnRLD4fa1YgbUlMd8jeqO9XcBDB4mVtDTxyeGa3vU= 38775530011374537813502898274019389132620116890266344603221997943675706375698597061571989090674289834838060050848545748579361837989319487970580969082824601965845786771062335733318139530316825802589479118956745739691326447349403950997231306042638797277408335778415717988679050762936401945487285814799382535925 +Y4BVPZ6necuLSwaqYEPeZp0lt9tTGFl/WCJJbwg7XpyvuwYKtzagC1NLzY5ymBfwGFw1yRlQuyGsYd9mBfC99DuVCIeh0JNrhJN1bNfoSzy5UO5+dmTr+dm66VGSRS0tFcViDTfCIleTV+zxo/xuZT+Bjxq7kZue8zGkjp42Kmo= 69872189501616471647606976308259279995249122669120675885925763529037695584466011511740991152346215507625265226811128801733353566555339153627478941716586678793853828514394269931890370517258825006937741437480128878717892485074131232336852490940507703859793477547154689914725314529986438108117871674332626168426 +AKCP9Mto4q/a2xNqM4N7PekbKspwt48OGPre+iqVwPrSP/jWKxg3CvvLNZzN5P+/FiUGIklMMFJ8w76OaHIPqKuwckj1gvCLECJEE+UAZWrNKPmpzd/ootN9/kQhNMuloTFCyhXAUUOXJ7Z0WVLb2u6fn4zroszSMBoWQEKC6lcq 112750701794692134675959811050012620191158543234019977304167102486465198271340022889272244811582365901584420008564301920174477182946432553537794834985703732129975734658113610563794129371053853971031300761815004524681756388784922001759202643614966614186697992611399618828963452661554240362943588548146868410154 +APOTAFA2waoAODECaGNgCHa8dNN+cjMnD01M+IeQFytzo9RLMzzzg/gpTUFpyLtFMcfbCkDYQMLXwE4crTimdz5sVvjGQ+5fSFQjoDY6Bw7MO6NAcLzlV/sI/1WyNBKaLQbcl2720n16tdUcdckQNnV+cC2J48CVxYM1c7QQlxA0 171043636512232272455501595416608280460445723238023572475354665686544174728784633443479486247342724860289312593374524429736857970220153680852977711594899595712511352458264354251161579203922747468321999465061463474727943140910084880926005209538535217464825087114791420210981711903880998556269523363208766099508 +AMGpxRlB8WVnsGqyyiy3/mzrPymtJW1o1HcDErK11ZwQV5PwTF3c0THwlnxDmcziLWHSWgPQwfRddVDCXMGW9BffJn+XO6aTcWDPmDAh+1DbWJPE1aqApGbHvQ8HONy90dQMZf1ayuwceWCVTuU1wnHdo9F/sIsRbuu7ic2OJDzY 135994898408425255747055209966103741651849229328236418804928584233229830656742052333413774490626915784901255640138520158698845938184666683995579777154437927013722740366497459963753542029774185193376253885864514386760437194444013834088425088260658670140534670789371556026135595577395047002643901630053097946328 +AJAw4uDYdSYkOrjtwJVWLv3pi1+UxWge4RmkWKqVquTsAVcT2tRZ+MFdHM457Hl7fmFIyxvGZQy4c2v1AbHEfPR8ID2sCRQpdcfrxEUZPMDqxfnHHm0ziny6W4X6ggdBzMp/sBWaVNTBL0e61/pELBGYNRGFMzGws7HQkr/sro1D 101254336834199527040756567675327011562230719161388328289463594628690618298993695452746353237675715087353241661592074446889034411683413957950360025295995263477031608845241728493807755308798509893719674568267846671753070163272328014412744008880395248474446310603301447848026040555910147467745595720879397834051 +AM09TdtXgYL4FI5CGNiVjf0T/AN/pZ5zZsBOi1MAUKMURiXnc1x8VKYTqM9Xb86mqNBBqphynIQG6/3e/YbGJgHlsSdrmKbo+P9daMr02I/7Z76/7Osa8+7Ky6lhVCbU3F0tBH4WvopkCQmuJ267afgvDD5kB+9uNr28deMH00cY 144124056591600568767398029380314564902309327093641173350205276895603332085753288682409279238417493662029954512382520307259348748813767324609446500382301421328754981718014234615523158887865271179104711373675849713359713282937065993613915015084108700238420759344034475478243507306107546245540340758766909867800 +AKDhK+/BKGXbrbBh2vM61OP8LN81YwlJKe68KNwUu4tjXlQg7i49Jis7QKPI/YFPUpSNTu5N2iCgeMnCX4+r3NAfivOao9lw4N3nc9bi839SIWdlokhwBHBYmCIgjehUeBAdkU4jKqlE06pIrpRmSvBtn7O4aWTbT+C++ViYAcGF 112973480670453665543892521898882856059335781900313607790238402438320486344365203510769919022496690291280873287383392088872774202832124927485754495093552572232234532821756395965072330282810574669371524103814871172318519695921477775100282448247625395376072233777533359104085023946019406729587713120941266551173 +ALxDiSxHjfxvP8ETvpE+SyDPTS7q3o3zCK519WTepygC58KSRfvDnIVIyV3toQKzgqD50kF1Ni5D/wuaSs62y3zg3kELX1g+WuBCc8+x50+kDtbHXa1Me3et/OqVS/QeppkcjK1UZMU29fXze6P/w6aQfvKQkE7koeQtZBKkYc0p 132203344567902304830160099595561253300484092355345272411265169562971473393256361094745618829297250316196312398486598077249124198329075791740755862221465178128527292695331061023291345396067863215552021206609309872689233899464919108147533679134727064586730810633196817136739658243232643507412032417747255282985 +VF0YUTvy8Mfi5o6X06DEvLm87r72mAtTdyyLNr0/GXlk0Xj3L2Oi2bVUMtcXQNRXg/mkdGP88pgdaP/eMzqkUU++vJ7t3UgOC1i3SHegpiBhhZh+aZHH/wjFV8Mz2XZB5z8MpMgN+QwALK1TT2Pyt/feQTsOy0imVanB5+OvCeQ= 59242171319056188000481457618922567543461456096441095927600135114274111606802456239311634638536207588762066940095527920532936960549439269891703098017342732142860571277442598349453761561189719823290643146391349978698217357430495238876700400634593256155537598291759795109752990651995982467695091946768443574756 +ezpwBt0N6QhTusiPcKrBvSB6yuk/KShTLUFQHdf5J1u1fgDYrp+aOWuXOFVfOd0bweiG4UxBQNXB2IDFWfYON0fBoaDqNk/41YyqXBSkKbiNWLi1y3zPmwTAiwK0PzYp2EPfk/t/j0HsDbvebu0ygcxb2tPqj4EQ1TXEdD007kU= 86533835313999945727720083706940213467453975054116752898416709637030456504024135513972566184073843025739226187558143854850980654667596935003124034699919861200483994576288766702308068265526535622439762454501169018136389983894783905946543636163866717367545972667876983557989192393479830223914708619684891389509 +U8BT26zT46tTZnkmTNxGUAlXbJhk5cNi4AMSd8fSvZHm55siMFGJ8Jl7mtdzEFR1UFAyEztf2fUhxdtMLe8ei/OJgM0j7myQ9STucEwnsShT7QS/DjBmfvcC42sl1CRpXkb0ZLrEJCPf+crtLKGrG7ExS1oawIAgALBiMQIL6mE= 58812148564290791415180898639607206220554150794356494356250223429674091688305329629529905854147200457536549527135776329004085047145097927266797668252160196098870200925284256433894773392353678965699083286106628662506590268955650280670838340651598082083455821825076016227525614626726458235627297885815646710369 +HfYii3U1SIkBZl09RHaGGA7H3np+qxyNeeCNY07PDl8LwZAaaYk/bHPeBVboan0I2X4o78zCD/gFXFBJ4rxwwUsVjHEioyO2JcpV2/oDOelJBD//78WzBMMSWt7ZKbJV9uYr9ZUM0BUD3fsk1esFCEdnDJdr86U0UMmiig2R+ME= 21039655953870571289679214995029926285040274249531458675115179004718812090027267801012507748013357317597416722235988917212676802092082137617336199787762782958420742299451435320649616271885264333948336627286638368859041172783505464468640994920853000441536629081040963398001710173320125308624362209157720438977 +AICOlee3daFyqTrTdtWjVb5M2rclh9BpIo1CRvKo2bF7NYcjrU0/VvbOnTVXDwdeGMLupbi76f0BrfDxYtkzMXvIZlgoTit4g5ennnklDHFBC5cogaGlri8U28w4/h5oMunZ1O4ezdpRgVJe9nTP/sSEMYiNS5IA7Zshdvm/XccF 90275777798511290102824338787811725003177532250296755103300529948194832904403489332420505850668003332750291879153080212231952155092379375422537931240723308384652734942204313672973885652497290433943089371705605128843469306776615573873479312715317072986990219294942040272550822460408702072075001377245051602693 +L0QUSVIjxvE201b1ztRZyOOxy8vkUz6626TH4tbLwXjjc+AhmrvplaVlavnOgHqve+/L18XNuAYP4BqdxIcWTx+yxBKm4ZS92dRJdcAtccvZpEJtYjdJvI6qbL5Ph6HluaVZwp4dyFyXuZOJGTfYdTb7PUWM0jNT/xsqyjxSQ2U= 33191267986826803728285073844005357792766429917696698533494382218509532051029343127452480789088572904364699220151221680328978554239767633887572649589456766209242252549993823283929686430100804479376247660556781589549613316880150951333982646510273364068770923588389668733632648346075516618646974067295703417701 +APlD9ECKJuACUmQUsbd2GTOpb2PgQVT08C/5hyNEVdA5bWoICX7epmoCKCybdolk+cfEBP6fSz33j+Vn8MbeiHBLdmF6ETbmcyOjldJ902MDvU8dqAa8IgEZN5Uh5x/xzN+3dqk9o0ji7yi291u90rpfIh85PPpDat2B4l5zs9i5 175040148659257809883308984693597046378367187659749953472629929701758633206586720399909808941145946314755491399962797299295431089674294356220216615950668954164397362123668926410543898553191541662075745481299747832013627018846822876386760538344447600390187421938699064459451308870669878673306013635576901916857 +KB7N0tE+A5vFhyrd/m6Qe1wTihkjqmBn+rinfmMAzRlvtxIBSyDLzQsOQs7L4oTG64ABU+YwcWVijvoeZNamaxGl4hatAH1pRqmC/r8FMvC4vqiFTbFHzQhkjM7uoHD1aKnxyBVgjMj0E0KZjrRxydZjIR2p13FXjLP3UQSFtII= 28173452509830313810392326357601136401754938805266458365469366750775669869895498658593356375710132149836430968810246171974040975430205200958564616924399794768861923079158311829444850822144940112488994119845741191519421434257276977333662656888696213514226866147767570046232093727585815615828360199830275208322 +bxFgV7eXwnbQScl4VzS3RTdcMW+NY6pcGkT1UsqHIeDVyBb8DnH/2/Z+DX3zniR1iW6FPdvhJJeQyPIax1ohILa11R27C1TLxGvTrRBGUycxjEcBIxamHveBsXbECWusYLEakeSDg9x4BTWMz1rTQajkorBoeEjYuW+xBxQtXME= 77994515143740690952370766995249847650881300682406161400195705464876513409097078624084133111941171517535435606295232558665316819077765607639545069239931096306624817379462598756505457054433358548941076472902905065316335603665413114267741896000877284610377452471067725794013283338924419969559537339967562669249 +AOH6E2eBzD76QdTJ6QbR/7OeF7AagUif9pEYx7fMqrIsXCJKKpLV/RHIItCDYP2WO4URCaVueoAJe3M/Shj4o6efvH9pf5Q8MLM0rn5MTHWhThivqYQDwjCp1ZsPgq1VFS+gcnmwgHhj2W7XzJxiNPeRXlxI2vL+XTT/wPBYhqEP 158686346608862569574095184731081143351413141116869402750758091813874232272198082464382169470744476593016502816563462778075467588097653320101723165887488327616477297401486647183409348122990505635004320879840358339260797834264972100385692477324858942142372580281421734058008608134075577990829273447077276721423 +ANDDgNXOB/rXwmS4KEjiHj7RCDocVrMv5SU0aw6AJzNTBfseFngqidXx2AJKOEeG7RDDN2gzn4K4qJktF0AIPG2JbELlLUu0MFlpOLxamp586qyp67Cl9OuPq3UZTyQhIsSIE3VQkvxuQkGsaV1owDV3BKIWQbQEqMQI3yT4ELHm 146598844784260148346676185962272439320781765598895126402049215152385925250917998794921584290777625240122575975327405909800121511343265147922400813488099624745229653124857224399973509428158163452130086943873214460600035260925149630502192183407327427517292065083168010281295559088633086659209316582810260124134 +Vprr6oBnWuxIzyTZjuxlKSdZhBc0upeNBHVIlXpQEnN1Q+XURKzp4/6Vg/koITftr3SMSgGpE7LkrERMGFgYaqM5XZ1RXYFKT9dRJnz9VRDITVZtdkDrU04bqo2Ur+jvZhvg/oHBDTgQ4nPLJfHO3+GEmUtck+g/wOVozMMgufY= 60816213163057201559480662231646403262735082707152897397414589876256824040344252799972529759737904461369360580708093117244392116003622336721789703580184437841209963565058475060017600871779929808204093448248984201640754565635410002090180110910120481044515630478472999135146756643143415057403006410330361346550 +do4LGsm0afQLHl9alWF2RVyEKPxLIErsf4pTPgScRE7ZiTSVErbCDeyzd/KHzhBLQs/DhHHcw+OXj541cIRm6jaLVKiT8EwLW/dVG0AkVli83sFh2f56Kk+bCGSKvfGEQcGLY2k7nQ06zoMlYR/xbZCka6Q6kSq4YBDQgigQ1lU= 83252051731120517035090523892596419800592471447735288551342681962005778435125655090199060145942826521644585427683714084736143440310518046334877897672493531918539106001203807757254797471481884534543367685912500572052457610702790097953420236852480969038388056545966568595395722585797418296411673622376893961813 +OL2Qoj4xkqRrQmuuLwrABG3BMMBNGjfBtVBNTdBf7g027Ghkk/z3aK3jKT1EPpdiOdn8zXYBSO1mTRGyK3n7Jo8ICOcnlBOF6cZtDsb9bvSVE26MOD2wzl6irU7vzS+s3vGBkN3AazrxPD4czk3xezA9y13DJVnNzgAgIQHEols= 39844525812817530522650122383059885756573694015271773938493414420875846359054562126060762455794481186614035892021706051863945033061233991184379580556219478200155757966121832613842937722944431875100059046588723473670448006803481527981834627086055642349130254917244469014754132003347635357123155857820000494171 +Ljgn+3Hcg5DOf6usRumk7P+ZrdTBRmo968HdZU1mS7LwLW3Hii2KNkwMV7J77zA0P1pnvhMSEEeh1RbCUjLtSIbt3RIcOEoc+aO0eINF8r99l83xF57CBI3MDA3AAbtaYATy/NUXSC2h4W5kdsQuR88139MFi5y8E5njqxHu3UI= 32456338403763561215581247445990611953939298888251578685087656354454727113846722731945605696397627662593375001096230320486703167389461057538581895745078593206660798580358701927596287363374862536765135996838944212622199018632046955402325290145163082309469649329852148345837780541107029165352782710901375425858 +AMt5/u+ZUNm+Xsucr4RQPUu6ExAOq/Jbcjm/Kb2YIAaEQ1czIL82wsu6YmpHcfMaxLjY+EnaaF+eCWQPeGd1av919+QFbQPeh5DT7ZT9klK7BFyVsN0nEDJQ3AMMJqq6lm4sUeVxDVTmMypYnkzRl7jqzyCRY1MHA+o2LyMECdOg 142886089970163885609957244378225169093559131065687633458877059657380607541767850701139140472705242750285722732461954100519608059127637509286558848391554697942686619832870045594188204522385787253648018847569919409782188708374165437385572046835539379151066214153911415525465041951116179326632238059135825466272 +AMvXeHCaa+zk5VdB27KoS8XpjSUngaw7Gwlq6e2RrkEOxBhU2rGWGJ3fhq1HBNRxDf0quqfYTMd1speisaEr3cIyx9BhYwB6A+Nex/Sf9DSixezhcgEz6c5CfwUYP0QTTOiZDqzz+GcjKikjN7DKJTO0WSXMRG8qX8FBbH0rlc9l 143142496664357119491819741364830737485524654099662921673419335301323845847085335210884201567922636945282124120681371777665458057821603161276185071778040317947168899788341482064834489328957963447735297898161379277478278414388733161844053774747425459239004132791029364174047523473372650441001639174571312926565 +AMxoMXHfE2i4khsAkv/lPtLQhbWUjP3kxYmlJkpacpicBB6z/TmG5zjmTC/sqzBvBn3J4UvMzKYFyk9/l7Wnuc480500a3S4HRVtMtirPueV8v/SPktL67eN2zoj1VZA/Rex0aRGjW2CzEKGwEn3G2bZSgdT8hKv7AypF69ppjz6 143539479941314279463880342636704987025205547180882175105616955926182352311179043850344463145750154442573797875223178075233807385237935671604701513551125937539235111702655902037518920150424691586943553275517626347557879039695678271564616114192941679606063184290901862703975921261779714258077775731727612132602 +ODvOKg7l9RCn5CePG1FfMitkR5l9+7JK67eU+WeA5p1YXCcKS8GbYAKCtXPD2QfxmQcrNYfAc6Yb/kksaq29oW7MzZuTDzK0HXY5xBc/fJzEuvU51gaI0PR3cuU1qRlLqwmIlyt16gto+2E64BgPgIKJcAjx+TfH/EqNeJ77/W4= 39488587053253042573878502921384752550143716864908041972426777545317969264945056510991363961916339225192727727267483337259701961148978214005913510275048195308792987888118270387288989623193626554910652030960235845935461155296845475356011099372367616732243132816329531758943935324760665826550992788664237161838 +AKkznyQtB+PGvbVroM5nUIzhJUjiNj7q4fC9sSFbmDgvehnwPElVlie6PimH2FKonGV4GSaxZ+osil+9omfkb4rO3pq8fy5KcFSw/gs09X/U2eEEcUt/4oSbjs2NaMIxQftM2CauULiwfkWdkMFTBkHnh7Bbyocc8dtmrBDdoI8a 118817437232756222334188081193205110010964766506378146125932730686679941224328135190204402802650523704343176483564284220367074983943319572348376466341132480772885833789613392397284313483009178508647973749522358005819092779831781339778163122774381387989185969990310049504391258988402795259963134610905036263194 +AJfwWA7XnYbTjlJt+9hO/Q/OubHkUkyMYrN6Jd0cN5MG9Rg8W3i8U6oJxT18p4XozkiOgPlF1lE7hIAW9KRKJKGTue+iw0okLq5UNMu2Ha6l5/wzKi0QzRVTBnQm2zjPlQpgUorBBty5mcbt/B/Y3vOE4I3iVXklVtjQ7zIBHaNK 106695084438708194568048926154027115609888551145480521213711726807296356271397749432698558860759334362315257102647885062353922543502466463770991058956633500180245599467233361812610650830611712448187310827443315947425061886163301613989593906515923245020641415290300558869209909418659128196109640872398602216266 +aCXItk5XhuNrbrqJr1Qm04U4y4AzSKDMms11PgVcdf5fCGdizibh6/oZqx5OitM26nRz2vob8F+ZIP0CIyIJU0T1M50dVTbbpwuVNdv/XI6gHekQt0d2g34x1TQJIcsT1VWwGWTPNMtht1hezBAYxwv105AGKnqdLiz04YAdEk0= 73134927546833985031652237686088635686032103401394612286045377544136784429757461671691980910279873140130943470029643791712859175007885735170485461366406852784845528918253441791024065848540598601036357817496637108534035807393364939272891745520961269029038360205258229770737579266643408540634722493263322616397 +APNeoaWlyNa554OtHP8F7GAY5V9F7LMoF2ssg5wBmsgGFktrRH1C4FdyD0COrzIb0Vcko1/HiTnA9JXlfGKc3gTHEnO0gxBSDjK41L+EIgUfR0EhAD9iftTaCoBM7qZN3R1MYrSz3sevQZNMFOOnRrzwWEXnJaPKAZXvsqPzOIF9 170899982929163229592439208307232242235219591108657660041403142612622997092685093132858257827585941687488772925553142105567685213341947938835403410054637382864108739466539574004149772568683507025358331323655651148107044968424043673850583150424463706583215452211942132017052425497789362680979074312857823248765 +ALhwBfBYpOk1pfJcNut0C2fEAd4hhYU03/ZQBqVe/7MgpEDjro7oMvSdba5kjH/VBssmZVqpvuZ5lG+vI9lXLukhwRKJg7m67HG8lZXvjDmjU/PCjxBPNt5r8/DziETYmMa+fhaMTw4hedZcwDe37t1VPIflvM94sBKu6be9yJAn 129516480651398210587505113546142851617282590236388547627336279692965778911450075230961856270046942312918567973875005814982283590898552829322178788678196583244198944578081007477482775130405341039067711963061287597331433268366003672643052056973656674139309732186091974604170508497340243515339072325943686631463 +c9vpoiZvtnj71b8XguD67WayOF57QgOX4V4L++nG2u/RY9VT2+0tJ/C4NIawVa7ScQZAPVLuhV4J50HJX7FZgtY5n+lwMzNo0av7i0IqTS+1BBO8eNJy2wkCbWWBxNybuNnF6OK7eXdPb2Mmwm2OmhN2/j7HAr0cD7rK/Hnif7I= 81358980280155473712258342299472964374474635149963153129588784719499494479288254287754874893180126149146558961101860327826747785201363745989346818037655063262173536227595206355647880155693272153902647256175878517626925488264893732295267833614283963802283320574654949992393798458265266551024756663538388467634 +APArEXNLzDydcHrieLDReJryWxFzcsN1dxjpJIVGeJp6itsJOrUtnmXVnETtaZhWsmN3/Zh0R7TgJ253f7PZ/Z2xCEdqF0hs2MmnERSywdWZQ0a0McbDUUaDjBNYFht1wvS6djbI1b8RfayrnEZ0miYdzrrP1ntU+5cM1QBAvj6T 168651870043094856205824264282870999215855903395882323164614939540734011037112413507417141209480771157672307388419164831992909066097194364645695794831939514470650008210390333649278806163193463937050083854756730458780288720541495880958909249273048328511615821480782977316719631334570687241232556472064072892051 +RhGyx6xibf0OvY1XjnmX5na3G7emG8PWbvEa1kIjR6pK6K1MrMZnxFefXpHWInFS7ADESNI9LHjZB8VW5QrjRVPMksgdEAlkhY7MyQxaclUlShFl2AfKYBfIIro+vg7mUMzMctD+07BLk+jejRHtPVIxHmNnZrZYds80ve5z3Xw= 49204219353786910100605282012781696579642953908541693903348594981245301165936599174304121350092894937817100350990938057159324959104937469442065996667276651025661016077514839755853073999975805394464570132481314896694678249282338429544941873047382467276103868995474424700207571657816852575364781281563515280764 +AKbFfU3GL6NILVyONPVD/X0tffk5HS//7FBp7n6JKMXu3VXvWnfTl32R0WyVHk2yP0iIyi6SUusSicOH9ncO8KJHmaoMGN9Fn+Zq94FTFqZne5NxHmCtwRAbFNDVGg4FeemGXEe1S5Kk1VcvWqnp+QgY0uwa7RtT8C7/T+1pZlwq 117110890075563714812929271250884717870581483065920538069845585667296154465072587148155060755111295509684258790280104272121160614620669593483929827848744548171793187278583947500205314283462739235860439216105116687015890394925743036369717346234391524403038196640934551590543386844279091801685432977718405127210 +AJ0xZ9dfRc6P4W31bMHBymgOq+38ETEIMvMtr+wB5WTcsquZY1IUB4IVkrHaOo3W2SIr479IfJOOQhmvyRS4iB05yDI88Z/fJfXarkH53gDivECuo+5+JmV7e0S6gCvOuVamwoQjlK3G32bCV2946ry4EyIsVZ6Alk9xk7X5HfGU 110384671994603894282707302829898242894456931176497230904862171369974466400767175784681299142670706023468915238955836087425993929524341269289746060546848852729416925808186253355106621584826213979718185296723694190658548757311188764342751280681935289121682174507629679900374674992438818324999211250580434317716 +fjzmb1D+YBU5Wn1GlwhxjiJS07k+fXxjeNRbOv5SjktzxOXmautO8xZ5ACOlYrTt5G2gzW2PU6sYNfByQ0xoUSyutOuQlD2r+8MnDrxCo6RxT3P0dUSX7q0IVj+oLK4GPbscnKLfe6KqUcYLMgKnDYnc+ztFD+csL6BQnM9WMLk= 88647261832601702291191332432291274285041869480562430895152086741320122435409959711452438332192792226899741738806447713240934608106883094466050154088410020909933636902495700779087737304255058561688767369900548260278700135161077055869478387490726087630962098228537973426295306997128615315548440548541717688505 +YDg99aHkQSh9RjytWknbXzcgLD8MrWUEHF46yQLHYANKXaQYyf3yGM9TYPCDUqWbOapqQe+XfOCoACLyRg7vVDsnOPRDI9ZFUgCQBNG06ZOxzktEhnNJoRC99da8jyodFqqk2f9UD1lVa8tsQdatjUDocwgJaDAOpYEyGnUlbXo= 67567767932654827067250684965667741848878457020992905661955722020937161710030993261011062929936964216357930453809610708591260182295097124272956485574313839759737390934220465669626974544253750900911093325004172643146669082793591441922014060981070503803266774197958528843445580649512373693546027107823355522426 +ANdsfO+cNtWsbT/QJHGkYAL2WCHWVPrX6oEz78pO8lUwiigVEow5roLI5Tm7GP7XffjF95z5WDxzpoam+Bfp4za75D6ZEHQmuFnpWQAmNLUHdKUE6UcsWN1rbV1uY+x+Nr5Vni/M7PfQi1yRTTJTYav40tFPb9rY48FsUotivoxd 151275723772668372472508916060743043308364940375633847663054782759325087560768667906829087958412643723335046123025802453213225972572697773468957759328009026531148112732519692142632237595562259864125679649273054426879080697360204352423668940795473103047320116317252295126635024518179060076282921965794883439709 +D2Z8YA0G/vzEVVQ6itLPUC92r9n9FKRpf6lDPWIgpZOOfIkukPp7zzTlo9Ej5IsBrZBbtGz/eYmlHeZ8Y9pQj8HFW24HeKYqjmR0ujbNxI0QgoE+VUwPVg0HhoQsOGmq47zpXpkDwpOAZbMh/t1Bafq6r2zM0qmiwOacJ8KFUas= 10814483230552506566705634583020057064935800294861277580077052473134972003523900930560478187758928889017740705417070994563709463926267126567504805864719383185267204810142444719634360655595490833208838383875687102074846353850310954150927702228780599083427768247170427544730791038729428517279760042619935478187 +XoZpSMHqlOyPYJS7dWSRNDJHCkjbo6+DECzu0FpB9O8bftcxan/06Twbo5d1lEqPlLx3w0XeWtrmCSCaeVcXVtlY3QuPjdKPv8LBnnhslPOVcbGyflaTPXU+ITWE6rwnIF+yWQl3NIwCV4EBtCT+3U//Dt/Ebif9gzfKpKltD6U= 66377743237695515693282032069691369056215169443985727092982918806809030742478033317158686828712146024066618073633406428345129492010236994055590530566431286733776441810601990431112187030942086686719669823512292071202675269428014136307286941704297995292544712278047959299939833088742083527714893795660235870117 +QUbbkyJQ0Nru9c/nPbphM6VxHp5DWlai6407KIDbTGvUReVYI7de1gO/BFphL9GA7gDareYoMuej3/SVp8lEujXywtXzjiI+j2TzR3YYiMBAMhsJO1wU9pxy69Cj5xeFFlrOycjE9sPS9nrqnEEEFNPiK/GDDTHj0KuNbWSCLrI= 45838919357034925862751142472777409057791233610959872523563363744902783251621354580995921495295078179996083468819097423327554678806691589090814275138081407920379810144694354354954459732280968086760894209634364189264517251735804373673532012530665557440070501687207620525228416650281363557992436992284712644274 +F+uI7ARCeAlnPLO1YR7RJj8LyhtE/EJMcY45lsNMff0YeENe8KOITZVxNA55FcxDYpg9sKi1UV3/ASqkqpH8MOxWpBdT2UwSX3oBkp6ETfJKqiag0C4MS8cQVsfcKF39BJ6KUE7X6KUEj11j2YIIRREmLPyZ0LatG7dN7Rmv2iI= 16797235966984072293396362937533957334369977688369659112225970370748312376722010874726300554329794854683394163379447263409228872034356195791733533528404245739693397078461712458035888813157166614479153484688995068722288153129390850561042173295997770817893349738328312152341860704179681230323810266038959856162 +ALkEoXznA7BJlBIfA3Avl9kygQcxexEMApwduVRiXeYG0uEXMQU4rgMJBlPqs+ly8LTIcLFaLnJAG2KFQn2GXz2TNa7w4xkegkrslIJEtBWX/lc7VzRtcLbhaXEs0Ci1ValnW9Up7dYOj3Qw9eNo/9M9b1fD9TI+0QXFtp1ge728 129924120553920201168632484268654219915712271781591182777925696006023100660478316445751842982460082888615429513674356810187315558964251402722465707617058251479494744427428152566665405423424700027316505872162698141109433045594670140335040479559124757490095995568556894332243767736124299898808796118800328801724 +Ki3FNTEE870E9GaNtbT418CLSmf++s6Di3hzAy8NgiDOFo+uuicJa54V3JNRxOBc99sl/chfZuaBQt14BFOQ0i+9rm2KD82okNABd+SNfXOb0Ow2taZX8CpkVJYDyphFPyHbPIKmzwMShNx9X2z9w4++tJgzBzGcFTPv1nhAlxc= 29618953883711174042338818332957726953262658484143534778541769862244883781157097499904047532839425875312731531093860721544220959674634750905085721866390609141599426547378130082409488797303960018348798930232014390380383063108812922828160584483043190739354817699497573863286563890071313017508437166939160221463 +AJq8tcSnAq6M32ViO4hVGiHY7Tb08cLVyxpl/v0Y5adYblvjrbsFcCmsNDi5PnBOBl5awR7KZdQ1xgq6jIs+SQbccEMvJvGUZW5MgcHrXBj9XVd+8oB0z0eahqXpgYBqLDeHLU6238xR3dJYFf+Xrcrzjg8swx66OmQKkAQVJtdq 108660120968150664552423780971948386965268856900017812123107864829782135741514930439461240950044759098603910762272795612101834680870627850178371693837566833495418727543557712057554231215186486008080050486837716071537742708913279026303380104388546316647349432118287628353129105425052237438199445863950767806314 +AI3mfrgcRwtE3mA12gSoQV1xyIGy/YA4pCCvja4mTjvzQOAfiZL0efadxZH5awohCC1SpZDCFsE9yYp4LugHKu/A8zMcp4k5ena8sTPDkSod1yucjybgmVJ5h17Pru28AzHQ/YUmCnojQv55aV2+AUhxzIfojY+NT2PKRqr+vuf+ 99645829268436288676280252226747461064597487404802430565833102291706103139410465131373666856042539909746769688396958963177805479987372681967013633920910376342526433530508868114301205524789149997372160919406352823342811006288909548557622230243808373083272214426118230701324879006645047374853535922112549545982 +TmXQ+D8XFKSclXwnTIH8d+sb1IV0gfm7GagJahaFL6A9rvYaZ0NTizkG5DQ0RmXyo0wPmLork/296whsdNdUxVAwnGFlWWvMV0ftR1fOvN9KoT0WtVZ4Rmu6Fuc7q1PskAZzIp7MkOAxILO4iX5dNuVC+GLZYIbpTel3Ga8fXuU= 55052751096768041533898435453266875315629605001878362193939750978427494147944918632414581744895066623527980497732722163665712245580312596487741856071020477624754815927936394948233480228964159047139170955663289543349257377302556035170334384320502468579367401821986660515827461352578142560630318492817238744805 +EF6KIBWQiQoHOnBdJs1p+WIcAv9ILt0cnQVo+o/2niOtI0C+eFBSiNgeddhotkQFgHvGUjq8BPYgtLC8A5IFKGzXu4SYj5ziagka0hqfhVs9zVHKNx2NUoMhPDG5R7+giwEGGPOayGHVNbsBf1FBYG91+mwy8hnNbhcHSnvLGk4= 11494909948912248031301686864833544028186348338729984264372557659364976118965740281229664413031002362633393381744365783802034700038490736736266032000546393704814403638058993380993275865674190555703046732456017652317200288968188655019374159412919163798248766655991273308390043613040731449231289437754791500366 +AL7wCh8tkFe07qChFAzRkrnNehvda/Teroj65X1Bmcr14+/zeJlZDObYRYBOm8YYSYNgJekcL3o9lLFE34sCMbSJgm4dGwpEVexiLVi+zc8ndnqBDSAnRqtC+3jbInm/v8l6cUvuzrUNtzXIQ/H4FrmPMiVy0EMerkMtkfw5GBsd 134080980697158076909534078193319899756347955848461100874771253577754225619652121295523443912922220564492468474647193062555347746840044705102003079330399499915801536721237211615317000955332058281901995149084303143543150689010335818219129745452688372571010816270728441637278434982752674030696337642893239393053 +APunLhlblRi3bbRBwSV8dsw8h5SvT8ncAmXPnca+e1dLzrQZzL7P2OhFope0mW1MCDl2kJPiGTdK3SiYJVsAFeR3r/0z96g3oq+8uS66T6VaJym0QToMsqQF4/fUMaTo9HsukyPyOgjVIU+6TiFd3SxQKIu1/GpQWVQIP2pkHFKM 176716779397275986910036615967409090183531310366246043951791503601618945774743601662530806467045971394247287367421508126613573039423674729894091424105133906122821596079925540513892022311039293333114333317886304014722168786051080135090242879622144693440448171583324154550086458411590240882982297314605229953676 +MM6B5AgdJKe5OLlPzcXwi9WhqQjx5KsnBYxxa3kWdGNTdk/IN6TVd4Ptn8lWkLm78mw3DXP4Ol1sQbIfkHRoKFUN6TaWg5aDCJBDXyHSTZI2FDc1di0Te1SwziYn0sIOe+R+rfuLuHlcT1xaZBgL6+dDLAZaZza36UEjn5i/pTs= 34273208848307582992498656582721015257885595139328466874135636009184357438445251703533153492315835793684794951576799764181908090765379592683793969576893243386892292517067596035059342970830813419330530731370385186653239446376170533147020072285887964430731437765184844167400169982662183791828762458682426369339 +AJK1dx77ZA4F0sYCgRL1LKSTvjGTKBHd4QBeVnE6FKJxIow82puqtsVZ7TBxbECex+LkLQPrEbuQaVr3giUDjg0aJCE0D9ZVXCUS06qulqcCCdWgGFHXDOQzTWDn6TlJCGxtTEMbMxSlUq1q0iKZ19kwMHiT3GydBn8/G7tIYd23 103022457217861194294329435482792508957642944252832971366936865663608381648431732294396977429863681671686490913575377744795372643599438468695483808375208871881849232129651519218503507811863794426234594709451104684234156597418383183271923307418704786548452806494411689822939919114966188329657999811363991575991 +fPZNsqUYBbVGA2FAiglnByxGJOZkVSpj8Y4QNW5wq6o/1e/PRwp0TLYJXIoCJRs82pAj0QDpQbHl5lCZmNxEIQP8o8xI//HCPxPIdgBJmSfm3VGetrOpqEGU0KJJqK4IsjoVpAfPFMUMOpGNz9CSvCHGk1AKrtYvrTJEKmETuig= 87751387019308584846595931543798879607048239290774788042055795835726250309378365187899578817976976035304304847968410200168743967600896348021636654074952051821111673620467434295067182213181329543946368332581250062140819766061014427755090798550122401239987766844126425179573454145697756278292448630509686471208 +EmT6DUd0bxcdprYhAnycQaxm89kltJOlIOGFFRmEK90H3RhzBGr5PRVTJVqemFVpVliO1gy1nPHgqDGVNIE1GXhrhyFJU6m+HJeNcduippRe38xPCiuraRkXao79X7WAiVYUq6RIH+UIRnfTvHBgzTwjrOvKJ5853hYmGaanjh0= 12917015385266582065020051081997430892582163827812227349569911846746592973268746845211126663077128575098045461893559476227689488349263954564361736197688317585888118974603264677576027836032271531903881104937422976121352854003385726888601980526287956222142458858211589791399646989299770657341412683499692330525 +APtOYyWzdY1A/YU0SGrtjPdMZA5E50Y3hJVXppwuuSk04TjXzcbu2Sqp7sMnKYbToRW4nB5p2UnaLPhTRy0yszOd1auLngW+0ttCybD6nTcVoP65gYOwXGfSEQysqKLb1OfV8kYq5Ba92Efn+CcWWWuS0wEr97W5M/Hccx9bGu0r 176473215292413922394356058789571494026727424839036665031567966488209592078148711908841964690807374236235612412767651029865069639786447019874344449598703213025389428836803984245755885691094364960118900160737925054803955567361126391353868279642836569627177281508980029006921064654964339077608785831304875404587 +Vs6bjpYfFA1R/QTeCfhMuZLZ+Zxo6wxq1jFZpi5SBR1LaUwAtOAj38OJC8L7zmxSOj/RGEmJHkulI3E1MH7P7xlWbY468/azfot5fX9BgHrtptV6Q0dkBUg7H91+tcxdbm4/V0HGQGa2rZp+XK1rO+U/d0ki6iNbsCsCR+OeyvI= 60957991334776853645581868230398759578123373154273044785333939425321390401088800849629483265841435899835570419798325123273632247193463641611211088549152950252041797959644227170492417662363676228611376046334386877555777556575818860902071813120592757466883038430756577949025778080997296219236534786815367760626 +GiauT9A+wmwJsFbS2OPIM6ultIbU+kT2NgACn1jFAy+vNBahdfHMCH0jJdCs5TbmKTCeiEf3ITc5TV1OSvIejJ0GRkTf80nY47TAhiP1aehZvMAv59NQHHTDUE1U4TPVYKIyFpm1V1A+JBHKJzuGrB4lvqB2ed7k4m/ZD5lFLMM= 18363925023885496669420377869542744504974590667921570026763131637088916425434675950812384919000566852243714758512996458727914094904422651029609645299422563453163291342992902510788457007623888307499601267675322986672697397389663297565071582648674012080122614260400848960757021864980761735684874056409664531651 +AL/9KOZLtZu4+ZQYQsmOgbST8F4RV4N/Z+l8qsbCFlHdXHqTTkcN0chsccE/3KkVTZsAnAyJqogbAvB/RZqttaK5a8iKlOEoerUS92FVQw/42WhsVaFggR9cHVuvCD6QqclZjSBQKQzUMy0YWPWlycAZDIv96tooA+V+Fk0jbcFs 134819194171226950171930028888667967094069342154233489571728632904658607624703819928943642011918061760802468868660586005724399808048609316802502143143910585363214684061242274402109137825176291816945489430125510625857564490981683683589784133305376252294774711594646923226452625156299996630452243345104727556460 +AK5x2N/4+PKlsW/fNrw76CnE+nS76Rd7Ugo3IKhMTB/IuCc5xG4MQHo5MlWE0oVkZ+Gs4CxUpvD/WCCjHHFlSxKG4mC6ehz3NVLglBt+f1RWfPkF28JPd0UaIOG3um8kG4J3JDN48PXOPP86A0H8ZYbE5+ImmXsGAcwvScUQRInU 122499245103202714319465533564374494931278163571999934877854825659720649344163774228004853964635693562785966889622928722984134944784141208867445419597834322541679973956606275877526560988151196822256754309120410807075405427166696093800381410682490767468563176131997424692783482903880902119461752084196789357012 +ALZ12i0hqFhwRAikcoahYzH/BUolhgZ9Jz6adLvvTO4wk6LLOpNC/zCz+LjM7HazZomT1SqeYJ2X+WeGFLADHuWo+Gp/I3S0UEneYHKJxoU7OoOtE0mB0BCncLckHao/LmbpnQpS+Lx5bRsr0yE6oWNea6gbyRm/R0to74MI3/KK 128128022342420083856194424802390993133863171077961467523372211039771843125192435716337829530528063182315478279257832480290950255315151577221042903861075751839976362752440630888566422581799720709574650482021111126414843635330535518992034746102956214991673417580508389225948159518319625680855827280146399752842 +APXxvLifWgehdwdTRAJP5KrchRzgbUsyMWKcPGm2ZkwGDJjoTl2LIOOGVFiL4CyPBxahkEHf0nMxBN5oNGX/Y4W4PuOAC8gMgHzdLkPWkpnTcyoe5DD+fQsqNuKVw9nvyB15fx8k0d6b056nfFjnnRqgybby7MSllAWSKRYRdxVm 172707950911363219032118650562553641123743396229371815589867086054370029540557395298194067635069298952836929253340374819975848769009260895874615676938511747311585257140973518651959463416682165208985512233703837931718385346209362040743041262031997793519095342415901373534535662377972036003546589624834285049190 +O+9ohtZ9SzGLJoZM8IRQAjhc/GPt2X5G+M22ZidYjx9WgOTrZDXorSyxLuHxay6djsJSgjxYMj8MuanYSn/DzPWBB1Gn4cDmIsfeYuzO+vUJ4l6d0nIvBg9Iqs61/PGFd46YxhnDiVQ9HEznyTjzESnNqc0+/OkQVJcwNHAcZBg= 42087920806448980363073662127262313840530298932643042322138035915324224188032438119079107631420338701086802583985117830416851550991102672642532160807467909040086448764318690465254898516502941122327185894900817634110254371864896139724173087625913998657136384357741816102965779105122269429701537815263708996632 +VJOZmvqrqsIUTQSSJpZPhbQIYN2tsfBhAciWnfAYpwjK9/ts7OP4Qgdp6T/V2EsSRPnfZ0VKdLg1CnEWDhfcODo+/BZcUrJ0AviFAEtdeUhoMSWXtjel9Ln2guHY4s33z2cN70+e8gfjes65lCzrxUIXEF4nKxzKBnScoooQP5k= 59391682001673484862915842850714742391303140646889359425353339320546979084250010101273851580028171449840778038774656177449549941659895629203970455580974953864068394275066532699748911169800076515776388213090834432354601344176559839798153004796057709798368011673585434643656820656931921831615507416411999846297 +FRyJCOgPziO6RDHX1JgYGZRcSAuoQFIZM4niD/B0twK3l+TRpmVigKZAJnZZFtmX+0JQkDwQn3lcBGQIL6mgy+j0hD58U2/Wd6xebuHSzf4OHVGo1cYoqZLplszA+hVCoDVTHi2YAZ+GtfQEggumcNVxqfEZd6D9Nu//hm0t21M= 14824975573460749317081504809641216868382341402512168178334301409725840669112911061147252565570697788806398498723577368905065980113760265945344671897779830912242224090954834750057278285419880820811348943398148063418809729356397202526234113316098584002071850758705282845646489058224513019380757604894853946195 +dUk5LyS7mduFJlvh5o8R73kJIeeTh0Zli/y3XjtIXfCaNRf+wDlD/pX91JEwsQ5Mvj8yq/Uq13QyWhoNwsPpXVcJtJ+02wtIn5darsBDfzcD/LbWhl7zTRUeMjZ72gAWi1djx94SWjrZJS2oWZU92Og1yOyKRG+ua0AhHfYYh6g= 82361050315899968537319599868832189063658136463903643442673674137187842597528653416212822014359684261704550279153006971937114135373937934986951573613797195556144113400128502946618028800530164890707031379614952207482505803377774320259789692177752930767589642007257364960987343146063216186985472686575891023784 +AI6rejwEznR35rIPuIz0CP2aWyhRUR3unJ90YfxyuVYxrqOJQGSDTSf6SGDDw5MqpZXa9pWuwpyrb6smOq4ZtC3Er7lipJfXDjhy+0k1qcfMjmqbATUscwXGpgW+MO71cttccEz6vhbjndi8gvG5M/vfL2l1jA8nXuBd4e254dbz 100186164434910864539376019601151338080943067893748898987236087770762310617199833479771711726248130012472861788210345311298499515751355424063761182369333224929721733015910055321263016834247318907562652286587380604998130368845939290804442878127169587599285040969551065995197981341260363722618429042861484922611 +AJ5vLZX0fSs8dUSBqd5hki48T9cYuR0atxR+qv7cRu9nD1vP8uNVR8dLitg3XH0RARt3ZmOgi/AuggZt6tTxuIBg+9JhBY9WW+BLL5CnYWHC3AKMi7MQBWciLtmBpyF152bDaEcV1PXxtml2KxX0Ba0C+hGVDmJSdi8Kjd4AkfU6 111256341508463539324514225759801553679558662737345522765042612717818066374840372549356543720386819501973783940451033901079765311790026584654529398345993992144903839534037331533660672892487693477412528974248713261092693018326068480417183236210881306241164169849090833681510163753605662526243408192127670285626 +ZhXtSzn1GiFfHHnSKUYZiTcEWqlI8owyCKFjCQ+VEvkdk50m8uN7RCQ6ZhI545tN7Uy0WdLstJhgJETBYLHHIoWsJn07mgPxuyO0XsqNroICMQEOO/YWQFk1c0VqZifcohQAwJj7fONzM7hTcA22/7gVigJ3iLq178jZOJsEPQs= 71686982768953132894579286530164112027530221141251507987469672039995314435159469907420372652392376452531392493658576814100773556880394271726970628960571077839124343525055625420896355363707908511865700866168843075071778015504724409171911254647909938237551680861008772396291072284353858575645679153885560978699 +Vc8Cw5m5yI+bJ5sUJYm/F2wyZ5x3D4ydyL0uU/3eVF2ZJu55OOlC9pUyyv7WGExClHvWpR9mhMnsqCLyseLfM2Q/YXJ7cjGPKp2xd+fvwHa4hRi1FdOxs96rJnb+HUt9hTwQByXgzpnUfs7AqrqaNf4WSlBNMu0IOOqDdB4iVHU= 60256873326783629723455608618518793848697944184579877638436234491615392142659293975260290798403892159720925893207048153291000664050780029732557737984085196691225472664027706406879051455184548871511448456651238810812870905640934953489289909009741493031472382758586341375517766302753448531830002512912250459253 +QmeUn6cbpE8YrDfMETz/+KVFaK+d4NHHzcdj/MnjcmqQSLpP/XwCW/aeudlN3SfKd6rNo1XZefunZO/ek+PHEIy899WzjiJaajhf2X05fl9WuPEaMES3Yrr+ClogFNQ+9jL8+7L+J8lDuqQzvchT0U0RPay5HSNZw+ZouVCiQ18= 46630904037845609335515965570673490721137364238213103678233212262384415738654627185220187275286458759154841820256007930773120637898228224906635911124921895934056288121005350040349882413280772888907627838315559544636626856478316691755270725623680935763476199888127096014398699432042227882284223578563208692575 +ALUBYIShA4w5kRUa6iNF8S33DqaprdOWjVBnO+j9CCGtUh+NNwfpKR8AKf536MtuFFtwaQvRIlkLpaTYXuRxzyU/YG2+UfRQF3pEmXQhcMxJqFzqZ5nWCIWlJ/KtYS4lcC/B7hD2UGAktnIdjVUTSxX60VzA+zxeunV2iBZXQlEs 127106299687401374061881872616647348819431126560557369258073443762502337592227172639640997680536372567116568811258505773087926491911004324918919511363985868314578663758269650473780772688462266790559846182685481907703974916356209771821075179827563487466641669110315430790405454641953880582274165368514679034156 +ANyAdMnVCVjmUZGiVdyvGE5mUQpKoJOJINqMAfzVUGvvxXFmGdoAx+xsDRNAP4KoijpXk6E3yPBPBZEWyhiHnyjEkktK/gX6gnb745afS0QIlsjhKCk/W/BHXkzC862Llnc1ZGAIsERnGceEoZHdICfDUh/7nMFp5WuSMzPB7nEO 154841617115465511611746667401422322067517612306328612547616471923266281876818466022676728696273611923942543658633762267658490816264271663863494188027433799849037906883352478212451733963905925106470599843045599411842850386623187980045961158399934160107237440980574028985561404965317132715808604373199725949198 +AJ4nfhDe+HojR2YrprDHW9FVUxsZvoIekwlNL2iKFRFcTB9IcEdh6QnGcaRinev7yEYUsL6saSxUj39uWlqo8udJFdszuuQUmnloIi34L5uj0m1OpLy2dawpFQr8pqyA7go4ugMMj6XCtiVnISUcK8wjHgY3Jed/EKK8k5ce0Jxt 111059703393618496515021583605572584329116596402705082562306930876194742195701060137568030171429700588269665205795898835699633817098262654446852249498668467827435829513531633390969638488553144849154126899372953755511962841193763362947708260103832329116485114451074371844037650417731807385491783373627950406765 +AL+heSTflb2MkRYFTKghfzqlVQ1oE5vcx0eCIsy9NJ2NGFXCRRvoGDVoB8UEsUWIRnaA+MIpwDKGpbOS8kRQrvBvPe/xM/t3jrGkaS6pN064+bCBx8Y/Jq31ZXNG8oUol+y1Eo6fkUKNl4EOetmZWK8VmhVwol5YngDffj4Q8ned 134567692290185631768518572983694048149859804864902017394351513816079806629664302312927579302025923096596995134868068794900003728293470554490807959649153000914807604036531509869958441069678002226922395630284261949256022972967357884468325217602330254290548618134453007903724438628204981673400911693835033278365 +AI272d2sbYIi637kHZC+6lievgcDvT5VKaCnus3fHwm2vfao7oYu31P4st9DlqPWJ635X6QtLkU5HgvVSy66MDj2fcOfwVL09ffkZYnoGNdhMADVgOq62Ro5cCpOdw8Ko0cCyVpVIaSysPuqY7kiClf9GTdyZz/uYHDgwWeNrc4R 99528854246023003959943182132914587584844397870416002887630245681136432049666385367430032197518895755482367603560037194955739661569172773017279832774100155646116233705958563163070414171045438199561777058338188494271322834524386565519620661180246416329082614115142485663975718653564590519408413408765689056785 +AN9S8vPzo4SkyKsk07nfyD0um1riJzRqqWF9KCL+kWMHajurgPACikYzu61tL7l1mNEaIU16Ndz541o+y76DgsTLYszu4KXUOEt1Gu3eHy05Fq18zCDlNesSVjkZjPmuJr2ku+p0cP0TLLMn7/KuVOm4GlEVc6OvBNZuEzRriSYZ 156823459768092337875922818543729136404805918580285507923139232733465414368775678369646914249412830351437211620056021568154043505276475345347569200977945836210758870414054407438380975491139001471954448623922841964684437333066353208837709613982022690623722155151315252634380695513434502419141555410441456920089 +AMc5H8kywLgiT4zz5xgoI90jejsHorbqUGtBeX9wke7zyvEKyWxRKScZwzRbinjDZzN48eg/30qTZOV2Rw97JFg+EA63iZ0vqfF8jErIt3hODniKX8zayCuNmiSb5kiZL0UDU1SNh8ER4m6o5vshBKkmqs0PeozfCGQtR3bZXlx4 139899247405256530335276706333424670310599977544642091674186635734421385499036688803073040921114325725234673132788498809189814711681909865484671959982394306416477300458309408833281654917008031099378445580498219376391819745965887864647387211647794422908411100892195529730435423964537342228510107659017578765432 +AKv+3H/TruTX3wdMWnLzD05em8u/QMl6lCHT4VkK+uZwBXoLeji54Tcs/hZIhj0Bdj0URrRt+7JdGSTy4Sr986AtVFxBJZA3lT+JT4JSrq3oY1Tv+tX/yg8ZodQmbpQyyfaFg3BgeHNmsUoCrdqhj4IwBqEXoOBRIXnzaTuqqSEw 120779384043726135670909127168686589868907326577918074234323699599475436892003731971700278391108690400460261929381703781833059801757700386671579819341589048987186473249926590758009001670959004477454905417357202448886738669226760846888369186457452643053236389556969071303251275912453385963613554945645058007344 +ANXIB+HxOyJd3YYsscMpqZpi/eYjZi5q6A0MohU4BiWEJK/E4uIObLJDH5yd4ng+hn7UMhc+R/AxG88hIdOc5NyG/QyFs95ZLUC26F9rkRifu2CBkgqR5EQi2cgwC8jGxQOkC62YND6cAn/ILsKTYaH0iavtO9Tz04vQp9Ypc82H 150122383481070201614242107655752525590609186454390549085509458064289390813495886095936526832230958746095739308601699615024239939948911472291507190108935262129646691795733786714291498653838550751365834947465294261687773081563139416397262227609481906371677917295227469553787085145970923979142676551778927103367 +ZQLFoW+dJ7vrHdMlcLRGKY6T6PZKnE2L3NjXymS/55my2CDBLdDf3oXwLlRjVt9KnEiXyQzLhyY2PrFA4k3N/3P5lVDLHero5c36TMshbHgbIKRGN2CGWPEFeQ4j040IwVbQCPJeuF3jL5ikCxWZFXfeEnTL6TqumLfD9yLQfKA= 70932215714423143395949105745758445705072524008235214324766464113352968998429901322485575506330607802260244612268338586532462314021433435523464635419846126736185176246740838082062856583684393425704173881940108783636582561707441482446854068022535943408999200681879161519209676205165680598258447492092651404448 +LzzvPw0FdtM2G/RRiqoajJiIH+Lw3jpL4H+08yOpp1bNITR2Aq0beu2nP0H4o2Z1/FNr2hzuGakkAhVbmmRXc8keoOkeaAQAP/8OYxHpjrqou3WPWaKx+vUCTSqVYYf8gnVKpAAC2cD+3lW+/ZJ538o+c0ovbUKNu1u1j1OBtA0= 33171669664542509840621265032202455391098253465550501094201777336478104142847268103467889435377685359857979277521589539506627375165485879405453566052091202280471235979376217319335800766353336252760793484157724210008639813552207624049019149744883918494762511376489708611103181576211531366514802868659603747853 +APrGj1lIIlxA57DNh+bTEAFbJK2Y2P3MxLShb4fPx2aY6j88k3umoe07ISQLf9PzNPeml4/0I3w0KNd2x4s9KHbj7NsIT64lhO6eQSEteqZXZGXUYUyNzhrTbAjt+Q9LVKItQhsTkTW2HTQ5RQZfGrkL118b/I18J4P+T8CGZdDz 176100632478477421621142147788721746818712752858710594712903769452749028606541677227413333567013253138397373757811889654342173021761934591400685421771460440213093509170325205622261487145789848227404883040799927313402244625239515162996390018403365063394514244196976794479529075569412676472840544017222373593331 +Fvcl/LemWk29I5LCjU1QedTjGlkvFF/kZXNkRJv+vNZ7qgq6pX8WB9yVkk6AoclDYAhCRfKTKuEpR23iafVuHpprPfNXcqBH8n01kq3U27xqIy2hS+D6BRBK67PQaekq31EB0aOcEb/DuNaXakS9+mtTMx6BKt+WoEY+NkzHK6c= 16126868736093163702771491576570380743773057522016869811780571865928979861357811080042796140032050364543242385458140594532945509386155523162799601656485075247603490060565663264947465987286983338572455184901756399862440455644131755848583379822279676555143231305246033911608913609591095831135803702269767527335 +AKW8tvaB8YZ7J5W2lmquBniJzUhRfqFdPZPqvBoMzR4cRh1CMNdSFsYsnsaF3KolNzogdsxFpHAaEMG6zSvpNJAoi4nixCqb5SETXrSLASXvNjI9MvCoE2JCRq7kMbjPL7cem+mBPWZITGUI6KVlJPLxQngHYSFxukqlx7jznwJH 116384596458828069344020651216200368975621068920641012055593076864629080375946542748377736186556382088448816531408136815533164209947323588157210859294774679831647934533061547276394884474877353537242203645373945111105805934070657589374883764420038511061919092743520704686962593876316976299391579463759429567047 +D5N2P4FrqDf7/2Z2BJsqah4SjUtolic/yNqdNzvNEogDKZKAJyGq4zhnHvkYXkEm2ueU/FDPJRqisszG0oULdU6c7p8acirEwsGLVh4RamnFRgmQSK1vbiYB3bR+P+iFX/bZ+TWjN2Y3YMa5UB//I6Zb5kEIjmTpjY2LEPI1e6s= 10937855369372570149476727082965401421189236366492771695094788039313362971972373068736123833330006002198346944149230147444718818161877123407713821100752433128205189334393732633989950841577315682292180735057952587083688644195300641998709155269462601925653013312848413290208844194513502358901613104779186502571 +V/A1ktS0xrcwlI8xrYqvlLCFYrdVp8tEzZaZ9iNNpPH/pzVsA0WbnnUeHbdilkje+4OdoX9C4U2xaOuWOfvqLR0c7GeCkSffCqyf4ZsBmjy/BQL6rCpxMF0gIHXO5O8aJ1h17hy9LTuNzWm4zVh4pNFuHC9L6nAcf92udMiIQzk= 61752386563628388546439207444896778638632243226541303179646524864765343154194512297447627825411023405896612559648434895675553567405277169056807223959390559391191382555701580549902639604424290133917402316755076644943742815711432111554988540913643347167948778404861099845961151998728662878854088239266688156473 +APoPgEKA0/r1FYmt/Iso6ChYK6dDU62Y+vH5h/LVE00biBYG1f7aL3GdllUTN+XQSHpqlDw8CD+9xojwZIMfgpgjOwLbbe7Aso460zLrg3R8aHBpbVt8iZUgjACwPYr5UyKbFzIAWaXcnYYQ+tCO9aDIuOz+/7eIF62C81zXFJVZ 175598490446477604563905754135475294999639698464908622773037381109011373179895295130424828038708319325919451724985361900259676699137657615076219968061941008972496322083528922054390781811699677037439989404270415929836486610353098273115864435328533577114470407444852521009919911888840405368858409835197558461785 +cL54ymLJhRx3U20Y9aUTIsXy9Ags+XHy4qk3F7uJyO46eiXSL7VrrR9vTQXAbETbu1YiVWfslsPht810eUDUVaVir6yLnXkywn46Ci42FEvVoTEFjO22uYcCh8nqB8H589w/+lVSlNrcILugwfdfCvK1iZzVimOO6l3qzfXToOU= 79171550718114578361958369278761819285111811576818442980166457146638966315793211967882077899426611721874954146020093740153495693185472340728106727284441726113022873005252623222594060645383105757498856463065370975867121188445567981809371870213273555432308279508351518168027875538720367440153667708369625129189 +QdQN4qW2QZq8/fmSaqlRiPSoDbhmF0oYjaY29HcKYGHdlOH0AMJb+RUIq1aszvVtjh7AYay2TNhaZMWQ6Qi3c42SNk3A1MVknT6zqiRCGjNFfxf/matbRLbTFQF832MAId708vrFLF/o2HpekMkc5hcHB6bkUUhEI1NLcMXwGck= 46226230186280253581676626651942823886592433541360244612432763620730826574920825070086312767146345247802570752482654580909236388357139147786783758670999083804670979821212991224400629053427330483809790366665043598754931511997925850227997764381723288657884346974360232490075739442406431704368767588177525348809 +cxHvCK/dyVDvaqCCQyLeaiBGA36mV5el+1lc2eUTkHGUzX5gU0QQCEp+iSXNJhIOON8VFpKOFsziuV0Z+3cegWRw/VnxnjXcBh6IDKdupzOPB+Yl8MA1ti/GrQjLC6ikcNYNjQT0ZThL7KTqEvvZJH68WYmD0IuK26swjNGIGaI= 80804939616399473443737611589382762718815989847332356984276911837267997590368701684135326680567847542004499684038240485603420973682522792156533112356849436451918522884749244246467852622918805139990256619014116276456718693703261686778030658826952213058982142604346352178078750879100976710761147710018148637090 +AIQ3OIZevkYoRGBmsFaXJobSfLeInuKKReVYNjP5VEPoMq0mXTltY6l09/rQ3d1JjsMD1PfA7emhxex+H9t3leBIfCi6Ux34GQEjXWpQc4awuiy9tbR077HaJyecvb8Qy1FTnOHoH5C043QJzrKYT/sFXjgB60piI8Y0R/hwxO4r 92845026347218330987427785323244729176754623818531419911990153715676845614711324345879159989637824921793015074978358052562420379797956750450245721653716740651389924718711940869162230097839047895842495414221110468446944827052871968998907462191349838598297775847512250220907563815783358238473966349820476321323 +LoG6ib5lUh57rdmSkZSWzBoudytFohS4uoU/uly6OaQDOi34GeNVxu/yr6RszJyL9JWkGNgFaBIv/HirH5zA9VQAL/6kpL93a0/GQ/nuHkHy3GWZPF/2+yJ0PfazQ40fWhHZfRxBngWslbguFPjj1XaJ37YzpQAYb/+QcUai9ic= 32658152290878644668906121702816147999633088014476055330179597550087921141413344679134407016170035735846077181424615228657687216737432274043674411132745299610950657139041836412322040866250189120286839287690983293111362228893996267791120043532014262644480689231457941173330523718758287779526551822788227954215 +AKu2jgOQCCfYZ3CLkXEH44aO4TtwMPeK/eq4FtNj9HZ9FxT0LLNJh0ZXPOaPJjgznvIw5C7/hNm7rUs1JeV8I8dj3nbS3EVERQz1gc/ckYB3H1bViWREOD5+TScDusi86YO/z4ar3dauKkg5kT1kKDuU/OP5kNMWvtJjHc4Vd3L3 120581042599355202025471829872601846477331097842315143148145881424071317426176264583672725691485724160094190478865850305422057632110749683552966861219554215519032344086824849470294473808177223497912069335635933312949412445851201918768630656712413082629164792850095444166888072453190903931430551124946191872759 +ANLs7OsR7oBM5jSjVADrk+Mx9d0TeieTIkxwWiJ5STKNQmW2EzPOjgbfcLhbYEhzzDFJveXO2dzz6/c8V5oW2yqg7VMx88DzEbpQnQpk/rOQRw9jbI4fxXNJHkNZCeysEVvFfLJb4ecsGA0xJ3Aylny/jP10ahPv2z5K99edGZSU 148116916208650944522110872759145096907599612943009577897396622287067669897712748449324334650112672914917664881091633448764667172850435775162090891556266912697811031318228334453406561952979778127173704706529448647577013482442758465809198730066784986763500579667100246958959793527011919373534159474250508506260 +AL+Er3n1qj+SBsZVtOMJYg4m0CN+DE6gRnC1F7nPvd2XnBe+QE0+LKfcpUDHVNxoydW4BDzNVwnUNbyjXZ+iuddPtO9hchVEI36UiuL0ydeldFpOZ9mtHJaAF6abd0MlHw4vXRf8CbOvXb5N4s76ggijlZBjRtU563sSmBcyq6Zt 134488725667189507159811764480908602790838430340670328479145818969651133017546803581865897303917708192047926432630297993507146075655594931523561067937580218599890162311074002344315818494246433967228889645359283635389151927472221799543158424012020308449895562192866672439712148770104592027035768027605661099629 +AK/04XOBSjjPpuFXTDF82RNWnKqZz9mJQbS2B5bn0ehFnBa6j+B+MazX+AxXTL/d5+hPLT1uexcnSMl3DcGGwKipOXg7Dtuj3pfJXHTrCqXAUYrIXI+8vKVQO55yQPGfzIg9SVgetwW1sDk+a28ZhJ5a9OddqNoi5C+dLce7ZtNb 123560902006294001923570614486104726169564351074482936927091682096999779538353161007361361829586988452098646362280351148131540524964916445100589671458589346440250329883789099771417949746709217272531950438336245613419967556433467843237384555807236658182067742367748737224684334525934210197178231424396818830171 +PzOEGHlihiveoWFAALY+LOfkRJfm0NUF/uR6cSU/tbpGAq4onNpr+iZIzEP5o3JBLOtDC595/NBPI0fzaXl0vQvgJs6KG8iKANjsLKQjIpZBkoKhdbG9MzTVQuAeuDW0w3sn2iMZ/v2dgAzRwfqmQYXJr3I2BbcwWraIJuZXw5A= 44381416070253681813077725822442106641846565789204187691647505370231831464947935035197059366680327425453811558282831465960889061956588244308214943856009686127871667376028831540813257349779756631357122923723235595360268572998278795110672666089470210929411514949652537714634611421849780859192966935514197771152 +APnuduN01GS9dO2m2uCLs400AR2lX7elOnIPC5U6e17qbukxWYzNhilZlM4kdGXAIeYpzFdSIW/gxRMZe6TXq9krFWRaaPyT2QwRfGHYnazS9F1QNYmW1zXdt+qVp0JGxmh5PyDstbP8Z3x50/E8Mb0gLLPhNAvzY2Jnr9A8Q1Hy 175507868985304663005133968393406051624825489142498103948374797086106732382869120248515993626061853699363294022457032257026588816021007648668265488426495800459085474654859258116280251546902009156490112550154951965894022789029787886785376415437170872937201839249103828294508088966180386198213606090453461193202 +QHEhL4iVzNdUsfG0izTEepwTOvxka8t/9MwuF1Ey6kxsI+ry4g4sJPgR2xMnbtOmvQn2NitAkfvA8JPCiL7a8+gmf+DVRDjKDfpfrtgAVmo+3rH+uJYTrKhAp8R7ggU2xIrvbIrgeUj7ieThPI3Rtap+IdkPCL853JC/+oKtytM= 45252649968839515171157821292772647085425694172492111870169593872127007254353374581972876464918186509502070064028725519394859148593053614163356612260257013360168930649423732336969778875205250872728821432415158634190866775855521719727700464116412886964736859295086745723651735554245035077902615220578218265299 +APeaekK4mVhEShCfM0mkRebcg1Iq5CgrFIEGOoh1nHzgebr5A9Wrhm9yD1Vd3e+fFD9urDRB4y5MHPJHX1U2NFToC+H8nQkFXL8bfd/9Wl2c7y8m0Mxwi53pLIdzETLbbfeOOtJvuSYYT3n8+/PeMnJ46UD8OfqtnFuS0/bVpFLS 173873040145444066957050580959132871919216036714423404143335635770937773583761934638398867981658394368476005882852706046614562314432695052874974848076542261910660410561876043187368112065303981001507235893831108658530338308496461162623683138693880482650786841100027392293758260448606244283355655751440485602002 +FqC/wgZDPTUoObPFSH5w4QR79zj/O+ZiHGTEnsBMwNZD3Gl/ClRDIsFMDDupNLgwgXsqCQbpwSOHOtAvUuAFwRpzt5B7lwIgtP5ism/AZRno5p+9WVSmUAM3glHsNtvYydz2MkXtnXzSMIR1ZVoLrdwMnckE4pbMzggqz+JZqxw= 15889870005716350976759704672045310928616256175405784574141006779373730686049218680335525720670897894546334915362899913262232170795516176419192840427996647372619000239408311568577050460995518058850793096827271653902583271225799114408537346367483775593212272587811309978019791973449354003275559762102731778844 +AJXNbv2AMWadF5h99ZAUy5gLnVK/hMaakFo0ZedtPNRJobxPmwj+h52G+Czd0U48G0V0wpdeUJC9v/4BhjzhCvNhNsdAT1+vQXDuteYQ1aspsEKLQ6b+NknO88QSbRJw53+KeOY2xe7PKOa4V89XnFFBF7wljRnIYrM8vvcqVQDk 105194875227030598769888785590198577650278341586165110611689226597424766274486797264032300493674927704016605741286512271390703088626381669060095573361828932336327125438452066548897528158329044309005232090053420259033538936293519762277428283316506398965916381374819450858053512398634116052299066189424983605476 +AIDRnUpBHepjBqYAlU4MG/8JxzX1mPxVNHpWvnEVgvqTQx/bisFPpXrYs3jAKIR/lzevYwhH0K/8Vvw4NK9iTMFqgSnU44AZztKsoxUXsEsl1UU56UscY5C7ciKU6vjjWI7nm/uHNOXdE82TQXkk2WX8ferNqZU5DaLFCb+zxb7w 90459642084794142567976043425270153270545560059973413835786695756473295513758287577749768786155290305189883600338986370836806413936196854410098516254596146039255388020628703824195128439558127783534033672712705194483515442668075394018677699876614329419492391568463215822656901183478205197671375262145069825776 +AIdvVNzJqWPgAShvi3GhbhMQft+SLigKGrhoqas2Saz/bA9u9Td6fAxa2LjrAqshW6cnm2aalc3Yv6RW/Y8vg7Ho31NSaRjT4zMUenykcC0/Y88UNxREi85wdnHwGytms6Lq49H8/7EFGJIyL1PLRWPmZn6XFkegscI/HUq/hiKm 95105613103051650721863964216778532448106311156426028879315612217763044797186635476805213120469258258125661666950525364331551671653846368977016286153840829836509696804585927581668281228810410814602664419962214359687545209312836366693384158782798559255789953908588601637765910472073600954502095647132310971046 +DdchOPjXrI6lpV84IdKCisPmdqZan8AARXRLADEhixsfXCYuO+WhNatI/fM1vgv+/TxwwIQjIfG1vOZcB36JUfjHYdItYQ70vUXaVFdpqvoBGyfOTU50Ds/11iGPCF8mWiQwR30/XAXytqDZtaVJVWsgHD3RigBSnSHhnvZAWYg= 9719024770319024562623340689338530708271347986326272393419504304391837979619189392867902307307106771234732135400958362219711925045600118964223238147375808749507928768896918369395426933218443166133187066167663170936604731896932630589251946733237697936733924510107175304126061649311812536190882160340308613512 +I+Z6rdTOt26/v3dtUP1plITb15fjb6aMDvqFS3AD1+nxBqnnk7ISGE9j6dv762EIWQpMzcCG5NCCq35KOHEwRXP28zup6olOMt3CBFgYVcBE2pWOpGiO19G/iFweYZXZPY5HgIkex7HBbb7l6HhomPc2sLL/IRhh2oogyHx2JMM= 25210054612455888156900839678249806510561198051210010474517915819801056434402727631042894881559517808906460418029149538469607239850657781476308872923928122553395468026744382526167194202058040459679991391557937527079948356545086684521068912222036707113005006607012596093923970784177288565193670152033981048003 +ALbBoyelCs4UkfnPjMT3S67ujhBHBEE0uxLx6kSGZq2IOMU/QdWYPFElRgYC/y++334FSEycjS6NAJJo2ITpZCO5AjNJ93J3WYgbDLiwu1VzKHX6ItfFNEk45km+QTi07+pDKcKNd1k0mxqpLd/PuZd5hRpPDDoKBb6i+mrCb2yF 128335905497646745013379107761994003743181143126608677203818152878840562628631384684712779135591095534911406031545494164782375276574093777950840330452805743803067864740000758175436633463846967335728314347497013853264454015790847388463800323796888198433722196292529074568758149650782323407298620158495364705413 +ANwlxEkeqmqYTxw1ZwMi1v2wo4ntPaEYZYoTLTJQfa+kuIksnHW9va243HAiOixd+rviVdm1dEwzESBbX0wiJNtRBpP+bnRxy4xOBjNoOB0c/tfka5JVwu5eeskyHx4V3inLviUaj86Yck42n5NaJFMfBvhzVftZ/YF9WBITI8g6 154592850289860621115358362871905683265658659789986179554827712019629689749439795961607030363152337159590319622241556795951071651584979664762468782303706550885785493534656062553770262954861884613383561063525714923031691298088562054236178003658891902606245782350998076658704876516153027797371814038658244397114 diff --git a/Tests/Auth/OpenID/data/openid.html b/Tests/Auth/OpenID/data/openid.html new file mode 100644 index 0000000..1a57d44 --- /dev/null +++ b/Tests/Auth/OpenID/data/openid.html @@ -0,0 +1,11 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html> + <head> + <title>Identity Page for Smoker</title> + <link rel="openid.server" href="http://www.myopenid.com/server" /> + <link rel="openid.delegate" href="http://smoker.myopenid.com/" /> + </head> + <body> + <p>foo</p> + </body> +</html> diff --git a/Tests/Auth/OpenID/data/test_discover_openid.html b/Tests/Auth/OpenID/data/test_discover_openid.html new file mode 100644 index 0000000..1a57d44 --- /dev/null +++ b/Tests/Auth/OpenID/data/test_discover_openid.html @@ -0,0 +1,11 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html> + <head> + <title>Identity Page for Smoker</title> + <link rel="openid.server" href="http://www.myopenid.com/server" /> + <link rel="openid.delegate" href="http://smoker.myopenid.com/" /> + </head> + <body> + <p>foo</p> + </body> +</html> diff --git a/Tests/Auth/OpenID/data/test_discover_openid2.html b/Tests/Auth/OpenID/data/test_discover_openid2.html new file mode 100644 index 0000000..a74c042 --- /dev/null +++ b/Tests/Auth/OpenID/data/test_discover_openid2.html @@ -0,0 +1,11 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html> + <head> + <title>Identity Page for Smoker</title> + <link rel="openid2.provider" href="http://www.myopenid.com/server" /> + <link rel="openid2.local_id" href="http://smoker.myopenid.com/" /> + </head> + <body> + <p>foo</p> + </body> +</html> diff --git a/Tests/Auth/OpenID/data/test_discover_openid2_xrds.xml b/Tests/Auth/OpenID/data/test_discover_openid2_xrds.xml new file mode 100644 index 0000000..8091ab9 --- /dev/null +++ b/Tests/Auth/OpenID/data/test_discover_openid2_xrds.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + > + <XRD> + <Service priority="10"> + <Type>http://specs.openid.net/auth/2.0/signon</Type> + <URI>http://www.myopenid.com/server</URI> + <LocalID>http://smoker.myopenid.com/</LocalID> + </Service> + </XRD> +</xrds:XRDS> diff --git a/Tests/Auth/OpenID/data/test_discover_openid2_xrds_no_local_id.xml b/Tests/Auth/OpenID/data/test_discover_openid2_xrds_no_local_id.xml new file mode 100644 index 0000000..e6a0eb9 --- /dev/null +++ b/Tests/Auth/OpenID/data/test_discover_openid2_xrds_no_local_id.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + > + <XRD> + <Service priority="10"> + <Type>http://specs.openid.net/auth/2.0/signon</Type> + <URI>http://www.myopenid.com/server</URI> + </Service> + </XRD> +</xrds:XRDS> diff --git a/Tests/Auth/OpenID/data/test_discover_openid_1_and_2.html b/Tests/Auth/OpenID/data/test_discover_openid_1_and_2.html new file mode 100644 index 0000000..5e58128 --- /dev/null +++ b/Tests/Auth/OpenID/data/test_discover_openid_1_and_2.html @@ -0,0 +1,11 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html> + <head> + <title>Identity Page for Smoker</title> + <link rel="openid2.provider openid.server" href="http://www.myopenid.com/server" /> + <link rel="openid2.local_id openid.delegate" href="http://smoker.myopenid.com/" /> + </head> + <body> + <p>foo</p> + </body> +</html> diff --git a/Tests/Auth/OpenID/data/test_discover_openid_1_and_2_xrds.xml b/Tests/Auth/OpenID/data/test_discover_openid_1_and_2_xrds.xml new file mode 100644 index 0000000..6d85d57 --- /dev/null +++ b/Tests/Auth/OpenID/data/test_discover_openid_1_and_2_xrds.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + xmlns:openid="http://openid.net/xmlns/1.0" + > + <XRD> + + <Service priority="10"> + <Type>http://specs.openid.net/auth/2.0/signon</Type> + <Type>http://openid.net/signon/1.1</Type> + <URI>http://www.myopenid.com/server</URI> + <LocalID>http://smoker.myopenid.com/</LocalID> + <openid:Delegate>http://smoker.myopenid.com/</openid:Delegate> + </Service> + </XRD> +</xrds:XRDS> diff --git a/Tests/Auth/OpenID/data/test_discover_openid_1_and_2_xrds_bad_delegate.xml b/Tests/Auth/OpenID/data/test_discover_openid_1_and_2_xrds_bad_delegate.xml new file mode 100644 index 0000000..db7282e --- /dev/null +++ b/Tests/Auth/OpenID/data/test_discover_openid_1_and_2_xrds_bad_delegate.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + xmlns:openid="http://openid.net/xmlns/1.0" + > + <XRD> + + <Service priority="10"> + <Type>http://specs.openid.net/auth/2.0/signon</Type> + <Type>http://openid.net/signon/1.0</Type> + <Type>http://openid.net/signon/1.1</Type> + <URI>http://www.myopenid.com/server</URI> + <LocalID>http://smoker.myopenid.com/</LocalID> + <openid:Delegate>http://localid.mismatch.invalid/</openid:Delegate> + </Service> + </XRD> +</xrds:XRDS> diff --git a/Tests/Auth/OpenID/data/test_discover_openid_and_yadis.html b/Tests/Auth/OpenID/data/test_discover_openid_and_yadis.html new file mode 100644 index 0000000..3befa6f --- /dev/null +++ b/Tests/Auth/OpenID/data/test_discover_openid_and_yadis.html @@ -0,0 +1,12 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html> + <head> + <title>Identity Page for Smoker</title> + <meta http-equiv="X-XRDS-Location" content="http://someuser.unittest/xrds" /> + <link rel="openid.server" href="http://www.myopenid.com/server" /> + <link rel="openid.delegate" href="http://smoker.myopenid.com/" /> + </head> + <body> + <p>foo</p> + </body> +</html> diff --git a/Tests/Auth/OpenID/data/test_discover_openid_no_delegate.html b/Tests/Auth/OpenID/data/test_discover_openid_no_delegate.html new file mode 100644 index 0000000..f5180b3 --- /dev/null +++ b/Tests/Auth/OpenID/data/test_discover_openid_no_delegate.html @@ -0,0 +1,10 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html> + <head> + <title>Identity Page for Smoker</title> + <link rel="openid.server" href="http://www.myopenid.com/server" /> + </head> + <body> + <p>foo</p> + </body> +</html> diff --git a/Tests/Auth/OpenID/data/test_discover_openid_ssl.xml b/Tests/Auth/OpenID/data/test_discover_openid_ssl.xml new file mode 100644 index 0000000..111945c --- /dev/null +++ b/Tests/Auth/OpenID/data/test_discover_openid_ssl.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + xmlns:openid="http://openid.net/xmlns/1.0" + > + <XRD> + + <Service priority="10"> + <Type>http://openid.net/signon/1.0</Type> + <URI>http://nossl.vroom.unittest/server</URI> + <openid:Delegate>http://smoker.myopenid.com/</openid:Delegate> + </Service> + <Service priority="11"> + <Type>http://openid.net/signon/1.0</Type> + <URI>https://ssl.vroom.unittest/server</URI> + <openid:Delegate>http://smoker.myopenid.com/</openid:Delegate> + </Service> + </XRD> +</xrds:XRDS> diff --git a/Tests/Auth/OpenID/data/test_discover_yadis_0entries.xml b/Tests/Auth/OpenID/data/test_discover_yadis_0entries.xml new file mode 100644 index 0000000..f161a0b --- /dev/null +++ b/Tests/Auth/OpenID/data/test_discover_yadis_0entries.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + xmlns:openid="http://openid.net/xmlns/1.0" + > + <XRD> + <Service > + <Type>http://is-not-openid.unittest/</Type> + <URI>http://noffing.unittest./</URI> + </Service> + </XRD> +</xrds:XRDS> diff --git a/Tests/Auth/OpenID/data/test_discover_yadis_2_bad_local_id.xml b/Tests/Auth/OpenID/data/test_discover_yadis_2_bad_local_id.xml new file mode 100644 index 0000000..68c2ce1 --- /dev/null +++ b/Tests/Auth/OpenID/data/test_discover_yadis_2_bad_local_id.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + xmlns:openid="http://openid.net/xmlns/1.0" + > + <XRD> + + <Service priority="10"> + <Type>http://specs.openid.net/auth/2.0/signon</Type> + <URI>http://www.myopenid.com/server</URI> + <LocalID>http://smoker.myopenid.com/</LocalID> + <LocalID>http://localid.mismatch.invalid/</LocalID> + </Service> + </XRD> +</xrds:XRDS> diff --git a/Tests/Auth/OpenID/data/test_discover_yadis_2entries_delegate.xml b/Tests/Auth/OpenID/data/test_discover_yadis_2entries_delegate.xml new file mode 100644 index 0000000..372955b --- /dev/null +++ b/Tests/Auth/OpenID/data/test_discover_yadis_2entries_delegate.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + xmlns:openid="http://openid.net/xmlns/1.0" + > + <XRD> + <CanonicalID>=!1000</CanonicalID> + + <Service priority="10"> + <Type>http://openid.net/signon/1.0</Type> + <URI>http://www.myopenid.com/server</URI> + <openid:Delegate>http://smoker.myopenid.com/</openid:Delegate> + </Service> + + <Service priority="20"> + <Type>http://openid.net/signon/1.0</Type> + <URI>http://www.livejournal.com/openid/server.bml</URI> + <openid:Delegate>http://frank.livejournal.com/</openid:Delegate> + </Service> + + </XRD> +</xrds:XRDS> diff --git a/Tests/Auth/OpenID/data/test_discover_yadis_2entries_idp.xml b/Tests/Auth/OpenID/data/test_discover_yadis_2entries_idp.xml new file mode 100644 index 0000000..9a07b3d --- /dev/null +++ b/Tests/Auth/OpenID/data/test_discover_yadis_2entries_idp.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + xmlns:openid="http://openid.net/xmlns/1.0" + > + <XRD> + <CanonicalID>=!1000</CanonicalID> + + <Service priority="10"> + <Type>http://specs.openid.net/auth/2.0/signon</Type> + <URI>http://www.myopenid.com/server</URI> + <openid:LocalID>http://smoker.myopenid.com/</openid:LocalID> + </Service> + + <Service priority="20"> + <Type>http://specs.openid.net/auth/2.0/server</Type> + <URI>http://www.livejournal.com/openid/server.bml</URI> + </Service> + + </XRD> +</xrds:XRDS> diff --git a/Tests/Auth/OpenID/data/test_discover_yadis_another_delegate.xml b/Tests/Auth/OpenID/data/test_discover_yadis_another_delegate.xml new file mode 100644 index 0000000..2f3b9af --- /dev/null +++ b/Tests/Auth/OpenID/data/test_discover_yadis_another_delegate.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + xmlns:openid="http://openid.net/xmlns/1.0" + > + <XRD> + + <Service priority="10"> + <Type>http://openid.net/signon/1.0</Type> + <URI>http://vroom.unittest/server</URI> + <openid:Delegate>http://smoker.myopenid.com/</openid:Delegate> + </Service> + </XRD> +</xrds:XRDS> diff --git a/Tests/Auth/OpenID/data/test_discover_yadis_idp.xml b/Tests/Auth/OpenID/data/test_discover_yadis_idp.xml new file mode 100644 index 0000000..f570d04 --- /dev/null +++ b/Tests/Auth/OpenID/data/test_discover_yadis_idp.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + xmlns:openid="http://openid.net/xmlns/1.0" + > + <XRD> + <Service priority="10"> + <Type>http://specs.openid.net/auth/2.0/server</Type> + <URI>http://www.myopenid.com/server</URI> + </Service> + </XRD> +</xrds:XRDS> diff --git a/Tests/Auth/OpenID/data/test_discover_yadis_idp_delegate.xml b/Tests/Auth/OpenID/data/test_discover_yadis_idp_delegate.xml new file mode 100644 index 0000000..5410600 --- /dev/null +++ b/Tests/Auth/OpenID/data/test_discover_yadis_idp_delegate.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + xmlns:openid="http://openid.net/xmlns/1.0" + > + <XRD> + <Service> + <Type>http://specs.openid.net/auth/2.0/server</Type> + <URI>http://www.myopenid.com/server</URI> + <openid:Delegate>http://smoker.myopenid.com/</openid:Delegate> + </Service> + </XRD> +</xrds:XRDS> diff --git a/Tests/Auth/OpenID/data/test_discover_yadis_no_delegate.xml b/Tests/Auth/OpenID/data/test_discover_yadis_no_delegate.xml new file mode 100644 index 0000000..fbd6734 --- /dev/null +++ b/Tests/Auth/OpenID/data/test_discover_yadis_no_delegate.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)" + > + <XRD> + <Service priority="10"> + <Type>http://openid.net/signon/1.0</Type> + <URI>http://www.myopenid.com/server</URI> + </Service> + </XRD> +</xrds:XRDS> diff --git a/Tests/Auth/OpenID/data/trustroot.txt b/Tests/Auth/OpenID/data/trustroot.txt new file mode 100644 index 0000000..1b6cb4c --- /dev/null +++ b/Tests/Auth/OpenID/data/trustroot.txt @@ -0,0 +1,149 @@ +======================================== +Trust root parsing checking +======================================== + +---------------------------------------- +20: Does not parse +---------------------------------------- +baz.org +*.foo.com +http://*.schtuff.*/ +ftp://foo.com +ftp://*.foo.com +http://*.foo.com:80:90/ +foo.*.com +http://foo.*.com +http://www.* +http://*foo.com/ +http://foo.com\/ +http://localhost:1900foo/ +http://foo.com/invalid#fragment +http://Ï€.pi.com/ +http://lambda.com/Λ + + + +5 +http:/// + +---------------------------------------- +15: Insane +---------------------------------------- +http://*/ +https://*/ +http://*.com +http://*.com/ +https://*.com/ +http://*.com.au/ +http://*.co.uk/ +http://*.foo.notatld/ +https://*.foo.notatld/ +http://*.museum/ +https://*.museum/ +http://www.schtuffcom/ +http://it/ +http://..it/ +http://.it/ + +---------------------------------------- +18: Sane +---------------------------------------- +http://*.schtuff.com./ +http://*.schtuff.com/ +http://*.foo.schtuff.com/ +http://*.schtuff.com +http://www.schtuff.com/ +http://www.schtuff.com./ +http://www.schutff.com +http://*.this.that.schtuff.com/ +http://*.foo.com/path +http://*.foo.com/path?action=foo2 +http://x.foo.com/path?action=foo2 +http://x.foo.com/path?action=%3D +http://localhost:8081/ +http://localhost:8082/?action=openid +https://foo.com/ +http://kink.fm/should/be/sane +http://beta.lingu.no/ +http://goathack.livejournal.org:8020/openid/login.bml + +======================================== +return_to matching +======================================== + +---------------------------------------- +45: matches +---------------------------------------- +http://foo.com/ HTTP://foo.com/ +http://*/ http://cnn.com/ +http://*/ http://livejournal.com/ +http://*/ http://met.museum/ +http://*:8081/ http://met.museum:8081/ +http://localhost:8081/x?action=openid http://localhost:8081/x?action=openid +http://*.foo.com http://b.foo.com +http://*.foo.com http://b.foo.com/ +http://*.foo.com/ http://b.foo.com +http://b.foo.com http://b.foo.com +http://b.foo.com http://b.foo.com/ +http://b.foo.com/ http://b.foo.com +http://*.b.foo.com http://b.foo.com +http://*.b.foo.com http://b.foo.com/ +http://*.b.foo.com/ http://b.foo.com +http://*.b.foo.com http://x.b.foo.com +http://*.b.foo.com http://w.x.b.foo.com +http://*.bar.co.uk http://www.bar.co.uk +http://*.uoregon.edu http://x.cs.uoregon.edu +http://x.com/abc http://x.com/abc +http://127.1/abc http://127.1/abc +http://10.0.0.1/abc http://10.0.0.1/abc +http://x.com/abc http://x.com/abc/def +http://*.x.com http://x.com/gallery +http://*.x.com http://foo.x.com/gallery +http://foo.x.com http://foo.x.com/gallery/xxx +http://*.x.com/gallery http://foo.x.com/gallery +http://localhost:8082/?action=openid http://localhost:8082/?action=openid +http://goathack.livejournal.org:8020/ http://goathack.livejournal.org:8020/openid/login.bml +https://foo.com https://foo.com +http://Foo.com http://foo.com +http://foo.com http://Foo.com +http://foo.com:80/ http://foo.com/ +http://foo.com/?x=y http://foo.com/?x=y&a=b +http://foo.com/x http://foo.com/x?y +http://mylid.net/j3h. http://mylid.net/j3h.?x=y +http://j3h.us http://j3h.us?ride=unicycle +https://www.filmclans.com:443/mattmartin/FilmClans https://www.filmclans.com/mattmartin/FilmClans/Logon.aspx?nonce=BVjqSOee +http://foo.com:80 http://foo.com +http://foo.com http://foo.com:80 +http://foo.com http://foo.com/ +http://foo.com/ http://foo.com +http://foo.com/ http://foo.com:80 +http://foo.com:80/ http://foo.com:80/stuff +http://foo.com:80/ http://foo.com/stuff + +---------------------------------------- +24: does not match +---------------------------------------- +http://*/ ftp://foo.com/ +http://*/ xxx +http://*.x.com/abc http://foo.x.com +http://*.x.com/abc http://*.x.com +http://*.com/ http://*.com/ +http://x.com/abc http://x.com/ +http://x.com/abc http://x.com/a +http://x.com/abc http://x.com/ab +http://x.com/abc http://x.com/abcd +http://*.cs.uoregon.edu http://x.uoregon.edu +http://*.foo.com http://bar.com +http://*.foo.com http://www.bar.com +http://*.bar.co.uk http://xxx.co.uk +https://foo.com http://foo.com +http://foo.com https://foo.com +http://foo.com:81 http://foo.com:80 +http://*:80 http://foo.com:81 +http://foo.com/?a=b http://foo.com/?x=y +http://foo.com/?a=b http://foo.com/?x=y&a=b +http://foo.com/?a=b http://foo.com/ +http://*.oo.com/ http://foo.com/ +http://foo.com/* http://foo.com/anything +http://foo.com http://foo.com:443 +https://foo.com https://foo.com:80 diff --git a/Tests/Auth/OpenID/data/urinorm.txt b/Tests/Auth/OpenID/data/urinorm.txt new file mode 100644 index 0000000..9e10135 --- /dev/null +++ b/Tests/Auth/OpenID/data/urinorm.txt @@ -0,0 +1,87 @@ +Already normal form +http://example.com/ +http://example.com/ + +Add a trailing slash +http://example.com +http://example.com/ + +Remove an empty port segment +http://example.com:/ +http://example.com/ + +Remove a default port segment +http://example.com:80/ +http://example.com/ + +Capitalization in host names +http://wWw.exaMPLE.COm/ +http://www.example.com/ + +Capitalization in scheme names +htTP://example.com/ +http://example.com/ + +Capitalization in percent-escaped reserved characters +http://example.com/foo%2cbar +http://example.com/foo%2Cbar + +Unescape percent-encoded unreserved characters +http://example.com/foo%2Dbar%2dbaz +http://example.com/foo-bar-baz + +remove_dot_segments example 1 +http://example.com/a/b/c/./../../g +http://example.com/a/g + +remove_dot_segments example 2 +http://example.com/mid/content=5/../6 +http://example.com/mid/6 + +remove_dot_segments: single-dot +http://example.com/a/./b +http://example.com/a/b + +remove_dot_segments: double-dot +http://example.com/a/../b +http://example.com/b + +remove_dot_segments: leading double-dot +http://example.com/../b +http://example.com/b + +remove_dot_segments: trailing single-dot +http://example.com/a/. +http://example.com/a/ + +remove_dot_segments: trailing double-dot +http://example.com/a/.. +http://example.com/ + +remove_dot_segments: trailing single-dot-slash +http://example.com/a/./ +http://example.com/a/ + +remove_dot_segments: trailing double-dot-slash +http://example.com/a/../ +http://example.com/ + +Test of all kinds of syntax-based normalization +hTTPS://a/./b/../b/%63/%7bfoo%7d +https://a/b/c/%7Bfoo%7D + +Unsupported scheme +ftp://example.com/ +fail + +Non-absolute URI +http:/foo +fail + +Illegal character in URI +http://<illegal>.com/ +fail + +Non-ascii character in URI +http://foo.com/ +fail
\ No newline at end of file diff --git a/Tests/Auth/Yadis/DiscoverData.php b/Tests/Auth/Yadis/DiscoverData.php new file mode 100644 index 0000000..816a06b --- /dev/null +++ b/Tests/Auth/Yadis/DiscoverData.php @@ -0,0 +1,153 @@ +<?php + +require_once "Auth/Yadis/Yadis.php"; +require_once "Tests/Auth/Yadis/TestUtil.php"; + +global $testlist; +$testlist = array( + // success, input_name, id_name, result_name + array(true, "equiv", "equiv", "xrds"), + array(true, "header", "header", "xrds"), + array(true, "lowercase_header", "lowercase_header", "xrds"), + array(true, "xrds", "xrds", "xrds"), + array(true, "xrds_ctparam", "xrds_ctparam", "xrds_ctparam"), + array(true, "xrds_ctcase", "xrds_ctcase", "xrds_ctcase"), + array(false, "xrds_html", "xrds_html", "xrds_html"), + array(true, "redir_equiv", "equiv", "xrds"), + array(true, "redir_header", "header", "xrds"), + array(true, "redir_xrds", "xrds", "xrds"), + array(false, "redir_xrds_html", "xrds_html", "xrds_html"), + array(true, "redir_redir_equiv", "equiv", "xrds"), + array(false, "404_server_response", null, null), + array(false, "404_with_header", null, null), + array(false, "404_with_meta", null, null), + array(false, "201_server_response", null, null), + array(false, "500_server_response", null, null) + ); + +function getExampleXRDS() +{ + return Tests_Auth_Yadis_readdata('example-xrds.xml'); +} + +global $example_xrds; +$example_xrds = getExampleXRDS(); + +global $default_test_file; +$default_test_file = 'test1-discover.txt'; + +global $discover_tests; +$discover_tests = array(); + +function readTests($filename) +{ + $data = Tests_Auth_Yadis_readdata($filename); + + if ($data === null) { + return null; + } + + $tests = array(); + foreach (preg_split("/\f\n/", $data) as $case) { + list($name, $content) = explode("\n", $case, 2); + $tests[$name] = $content; + } + return $tests; +} + +function getData($filename, $name) +{ + global $discover_tests; + if (!array_key_exists($filename, $discover_tests)) { + $data = readTests($filename); + if ($data === null) { + return null; + } + $discover_tests[$filename] = $data; + } + + $file_tests = $discover_tests[$filename]; + + if (array_key_exists($name, $file_tests)) { + return $file_tests[$name]; + } else { + return null; + } +} + +function fillTemplate($test_name, $template, $base_url, $example_xrds) +{ + $mapping = array( + array('URL_BASE/', $base_url), + array('<XRDS Content>', $example_xrds), + array('YADIS_HEADER', 'X-XRDS-Location'), + array('NAME', $test_name)); + + foreach ($mapping as $pair) { + list($k, $v) = $pair; + $template = str_replace($k, $v, $template); + } + + return $template; +} + +function generateSample($test_name, $base_url, + $_example_xrds = null, $filename = null) +{ + global $example_xrds, $default_test_file; + + if ($_example_xrds === null) { + $_example_xrds = $example_xrds; + } + + if ($filename === null) { + $filename = $default_test_file; + } + + $template = getData($filename, $test_name); + + if ($template === null) { + return null; + } + + return fillTemplate($test_name, $template, $base_url, $_example_xrds); +} + +function generateResult($base_url, $input_name, $id_name, $result_name, $success) +{ + $input_url = $base_url . $input_name; // urlparse.urljoin(base_url, input_name) + + // If the name is null then we expect the protocol to fail, which + // we represent by null + if ($id_name === null) { + // assert result_name is null + return array($input_url, null); // DiscoveryFailure + } + + $result = generateSample($result_name, $base_url); + list($headers, $content) = explode("\n\n", $result, 2); + $header_lines = explode("\n", $headers); + $ctype = null; + foreach ($header_lines as $header_line) { + if (strpos($header_line, 'Content-Type:') === 0) { + list($temp, $ctype) = explode(":", $header_line, 2); + $ctype = trim($ctype); + break; + } + } + + $id_url = $base_url . $id_name; + + $result = new Auth_Yadis_Yadis(); + $result->uri = $id_url; + if ($success) { + $result->xrds_uri = $base_url . $result_name; + } else { + $result->xrds_uri = null; + } + $result->content_type = $ctype; + $result->body = $content; + return array($input_url, $result); +} + +?>
\ No newline at end of file diff --git a/Tests/Auth/Yadis/Discover_Yadis.php b/Tests/Auth/Yadis/Discover_Yadis.php new file mode 100644 index 0000000..8f308cf --- /dev/null +++ b/Tests/Auth/Yadis/Discover_Yadis.php @@ -0,0 +1,232 @@ +<?php + +require_once "PHPUnit.php"; +require_once "Tests/Auth/Yadis/DiscoverData.php"; +require_once "Auth/Yadis/Yadis.php"; +require_once "Auth/Yadis/HTTPFetcher.php"; + +global $__status_header_re; +$__status_header_re = '/Status: (\d+) .*?$/m'; + +function mkResponse($data) +{ + global $__status_header_re; + + $matches = array(); + $status_mo = preg_match($__status_header_re, $data, $matches); + list($headers_str, $body) = explode("\n\n", $data, 2); + $headers = array(); + foreach (explode("\n", $headers_str) as $line) { + list($k, $v) = explode(":", $line, 2); + $k = strtolower(trim($k)); + $v = trim($v); + $headers[$k] = $v; + } + $status = intval($matches[1]); + $r = new Auth_Yadis_HTTPResponse(null, $status, $headers, $body); + return $r; +} +class TestFetcher { + function TestFetcher($base_url) + { + $this->base_url = $base_url; + } + + function get($url, $headers = null) + { + $current_url = $url; + while (true) { + $parsed = parse_url($current_url); + $path = substr($parsed['path'], 1); + $data = generateSample($path, $this->base_url); + + if ($data === null) { + return new Auth_Yadis_HTTPResponse($current_url, + 404, + array(), + ''); + } + + $response = mkResponse($data); + if (in_array($response->status, array(301, 302, 303, 307))) { + $current_url = $response->headers['location']; + } else { + $response->final_url = $current_url; + return $response; + } + } + } +} + +class BlankContentTypeFetcher { + function get($url, $headers=null) + { + return new Auth_Yadis_HTTPResponse( + $url, 200, array("Content-Type" => ""), ''); + } +} + +class NoContentTypeFetcher { + function get($url, $headers=null) + { + return new Auth_Yadis_HTTPResponse($url, 200, array(), ''); + } +} + +class MockFetcher { + function MockFetcher() { + $this->count = 0; + } + + function get($uri, $headers = null, $body = null) + { + $this->count++; + if ($this->count == 1) { + $headers = array(strtolower('X-XRDS-Location') . ': http://unittest/404'); + return new Auth_Yadis_HTTPResponse($uri, 200, $headers, ''); + } else { + return new Auth_Yadis_HTTPResponse($uri, 404); + } + } +} + +class TestSecondGet extends PHPUnit_TestCase { + function test_404() + { + $uri = "http://something.unittest/"; + $response = null; + $fetcher = new MockFetcher(); + $this->assertTrue( + Auth_Yadis_Yadis::discover($uri, $response, $fetcher) === null); + } +} + +class _TestCase extends PHPUnit_TestCase { + var $base_url = 'http://invalid.unittest/'; + + function _TestCase($input_name, $id_name, $result_name, $success) + { + $this->input_name = $input_name; + $this->id_name = $id_name; + $this->result_name = $result_name; + $this->success = $success; + $this->fetcher = new TestFetcher($this->base_url); + parent::PHPUnit_TestCase(); + } + + function setUp() + { + list($this->input_url, $this->expected) = generateResult($this->base_url, + $this->input_name, + $this->id_name, + $this->result_name, + $this->success); + } + + function runTest() + { + if ($this->expected === null) { + $result = Auth_Yadis_Yadis::discover($this->input_url, + $this->fetcher); + $this->assertTrue($result->isFailure()); + } else { + $result = Auth_Yadis_Yadis::discover($this->input_url, + $this->fetcher); + + if ($result === null) { + $this->fail("Discovery result was null"); + return; + } + + $this->assertEquals($this->input_url, $result->request_uri); + + $msg = 'Identity URL mismatch: actual = %s, expected = %s'; + $msg = sprintf($msg, $result->normalized_uri, $this->expected->uri); + $this->assertEquals($this->expected->uri, $result->normalized_uri, $msg); + + $msg = 'Content mismatch: actual = %s, expected = %s'; + $msg = sprintf($msg, $result->response_text, $this->expected->body); + $this->assertEquals($this->expected->body, $result->response_text, $msg); + + $this->assertEquals($this->expected->xrds_uri, $result->xrds_uri); + $this->assertEquals($this->expected->content_type, $result->content_type); + } + } + + function getName() + { + if ($this->input_url) { + return $this->input_url; + } else { + return $this->input_name; + } + } +} + +class Tests_Auth_Yadis_Discover_Yadis extends PHPUnit_TestSuite { + + function getName() + { + return "Tests_Auth_Yadis_Discover_Yadis"; + } + + function Tests_Auth_Yadis_Discover_Yadis() + { + global $testlist; + + foreach ($testlist as $test) { + list($success, $input_name, $id_name, $result_name) = $test; + $this->addTest(new _TestCase($input_name, $id_name, $result_name, $success)); + } + } +} + +class Tests_Auth_Yadis_Discover_Yadis_ContentTypes extends PHPUnit_TestCase { + function test_is_xrds_yadis_location() + { + $result = new Auth_Yadis_DiscoveryResult('http://request.uri/'); + $result->normalized_uri = "http://normalized/"; + $result->xrds_uri = "http://normalized/xrds"; + + $this->assertTrue($result->isXRDS()); + } + + function test_is_xrds_content_type() + { + $result = new Auth_Yadis_DiscoveryResult('http://request.uri/'); + $result->normalized_uri = $result->xrds_uri = "http://normalized/"; + $result->content_type = Auth_Yadis_CONTENT_TYPE; + + $this->assertTrue($result->isXRDS()); + } + + function test_is_xrds_neither() + { + $result = new Auth_Yadis_DiscoveryResult('http://request.uri/'); + $result->normalized_uri = $result->xrds_uri = "http://normalized/"; + $result->content_type = "another/content-type"; + + $this->assertTrue(!$result->isXRDS()); + } + + function test_no_content_type() + { + $fetcher = new NoContentTypeFetcher(); + $result = Auth_Yadis_Yadis::discover("http://bogus", $fetcher); + $this->assertEquals(null, $result->content_type); + } + + function test_blank_content_type() + { + $fetcher = new BlankContentTypeFetcher(); + $result = Auth_Yadis_Yadis::discover("http://bogus", $fetcher); + $this->assertEquals("", $result->content_type); + } +} + +global $Tests_Auth_Yadis_Discover_Yadis_other; +$Tests_Auth_Yadis_Discover_Yadis_other = array( + new Tests_Auth_Yadis_Discover_Yadis_ContentTypes() + ); + +?>
\ No newline at end of file diff --git a/Tests/Auth/Yadis/ParseHTML.php b/Tests/Auth/Yadis/ParseHTML.php new file mode 100644 index 0000000..aae9ad7 --- /dev/null +++ b/Tests/Auth/Yadis/ParseHTML.php @@ -0,0 +1,88 @@ +<?php + +/** + * Tests for the Yadis HTML parsing functionality. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'Tests/Auth/Yadis/TestUtil.php'; +require_once 'Auth/Yadis/ParseHTML.php'; +require_once 'PHPUnit.php'; + +class Tests_Auth_Yadis_ParseTest extends PHPUnit_TestCase { + function Tests_Auth_Yadis_ParseTest($case) + { + list($result, $comment, $html) = $case; + + $this->result = $result; + $this->comment = $comment; + $this->html_string = $html; + $this->parser = new Auth_Yadis_ParseHTML(); + } + + function getName() + { + return $this->comment; + } + + function runTest() + { + $value = $this->parser->getHTTPEquiv($this->html_string); + + if ($this->result == "EOF") { + $this->assertTrue($value === null); + } else if ($this->result == "None") { + $this->assertTrue($value === null); + } else { + $this->assertEquals($this->result, $value); + } + } +} + +class Tests_Auth_Yadis_ParseHTML extends PHPUnit_TestSuite { + + function getName() + { + return "Tests_Auth_Yadis_Parse"; + } + + function parseTests($s) + { + $tests = array(); + + $cases = preg_split("/\f\n/", $s); + + foreach ($cases as $case) { + // Split the case text on newline, and keep the first two + // lines and re-join the rest (those are the HTML). + $parts = explode("\n", $case); + $result = $parts[0]; + $html_comment = $parts[1]; + $html_string = implode("\n", array_slice($parts, 2)); + $tests[] = array($result, $html_comment, $html_string); + } + + return $tests; + } + + function Tests_Auth_Yadis_ParseHTML() + { + $test_data = Tests_Auth_Yadis_readdata('test1-parsehtml.txt'); + + $test_cases = $this->parseTests($test_data); + + foreach ($test_cases as $case) { + $this->addTest(new Tests_Auth_Yadis_ParseTest($case)); + } + } +} + +?>
\ No newline at end of file diff --git a/Tests/Auth/Yadis/TestUtil.php b/Tests/Auth/Yadis/TestUtil.php new file mode 100644 index 0000000..8936c0e --- /dev/null +++ b/Tests/Auth/Yadis/TestUtil.php @@ -0,0 +1,30 @@ +<?php + +/** + * Utilites for test functions + */ + +function Tests_Auth_Yadis_datafile($name, $reader) +{ + $path = dirname(realpath(__FILE__)); + $sep = DIRECTORY_SEPARATOR; + $filename = $path . $sep . 'data' . $sep . $name; + $data = $reader($filename); + if ($data === false) { + $msg = "Failed to open data file: $name"; + trigger_error($msg, E_USER_ERROR); + } + return $data; +} + +function Tests_Auth_Yadis_readdata($name) +{ + return Tests_Auth_Yadis_datafile($name, 'file_get_contents'); +} + +function Tests_Auth_Yadis_readlines($name) +{ + return Tests_Auth_Yadis_datafile($name, 'file'); +} + +?> diff --git a/Tests/Auth/Yadis/XRDS.php b/Tests/Auth/Yadis/XRDS.php new file mode 100644 index 0000000..466b207 --- /dev/null +++ b/Tests/Auth/Yadis/XRDS.php @@ -0,0 +1,249 @@ +<?php + +/** + * XRDS-parsing tests for the Yadis library. + */ + +require_once 'PHPUnit.php'; +require_once 'Auth/Yadis/XRDS.php'; +require_once 'Auth/Yadis/XRIRes.php'; +require_once 'Auth/Yadis/XRI.php'; +require_once 'Tests/Auth/Yadis/TestUtil.php'; + +class Tests_Auth_Yadis_XRDS extends PHPUnit_TestCase { + + function test_good() + { + $files = array( + 'brian.xrds' => 1, + 'pip.xrds' => 2 + ); + + foreach ($files as $filename => $service_count) { + $xml = Tests_Auth_Yadis_readdata($filename); + $xrds = Auth_Yadis_XRDS::parseXRDS($xml); + + $this->assertTrue($xrds !== null); + + if ($xrds) { + $this->assertEquals(count($xrds->services()), $service_count); + } else { + $this->fail("Could not test XRDS service list because the ". + "XRDS object is null"); + } + } + } + + function test_good_multi() + { + $xml = Tests_Auth_Yadis_readdata("brian.multi.xrds"); + $xrds = Auth_Yadis_XRDS::parseXRDS($xml); + $this->assertTrue($xrds !== null); + $this->assertEquals(count($xrds->services()), 1); + $s = $xrds->services(); + $s = $s[0]; + + $types = $s->getTypes(); + + $this->assertTrue(count($types) == 1); + $this->assertEquals('http://openid.net/signon/1.0', + $types[0]); + } + + function test_good_uri_multi() + { + $xml = Tests_Auth_Yadis_readdata("brian.multi_uri.xrds"); + $xrds = Auth_Yadis_XRDS::parseXRDS($xml); + $this->assertTrue($xrds !== null); + $this->assertEquals(1, count($xrds->services())); + } + + function test_uri_sorting() + { + $xml = Tests_Auth_Yadis_readdata("uri_priority.xrds"); + $xrds = Auth_Yadis_XRDS::parseXRDS($xml); + $services = $xrds->services(); + $uris = $services[0]->getURIs(); + + $expected_uris = array( + "http://zero.priority/", + "http://one.priority/", + "http://no.priority/" + ); + + $this->assertEquals($uris, $expected_uris); + } + + function test_bad() + { + $this->assertTrue(Auth_Yadis_XRDS::parseXRDS(null) === null); + $this->assertTrue(Auth_Yadis_XRDS::parseXRDS(5) === null); + $this->assertTrue(Auth_Yadis_XRDS::parseXRDS('') === null); + $this->assertTrue(Auth_Yadis_XRDS::parseXRDS('<html></html>') === + null); + $this->assertTrue(Auth_Yadis_XRDS::parseXRDS("\x00") === null); + } + + function test_getCanonicalID() + { + $canonicalIDtests = array( + array("@ootao*test1", "delegated-20060809.xrds", + "@!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01"), + array("@ootao*test1", "delegated-20060809-r1.xrds", + "@!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01"), + array("@ootao*test1", "delegated-20060809-r2.xrds", + "@!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01"), + array("@ootao*test1", "sometimesprefix.xrds", + "@!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01"), + array("@ootao*test1", "prefixsometimes.xrds", + "@!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01"), + array("=keturn*isDrummond", "spoof1.xrds", null), + array("=keturn*isDrummond", "spoof2.xrds", null), + array("@keturn*is*drummond", "spoof3.xrds", null), + // Don't let IRI authorities be canonical for the GCS. + array("phreak.example.com", "delegated-20060809-r2.xrds", null) + // TODO: Refs + // ("@ootao*test.ref", "ref.xrds", "@!BAE.A650.823B.2475") + ); + + foreach ($canonicalIDtests as $tupl) { + list($iname, $filename, $expectedID) = $tupl; + + $xml = Tests_Auth_Yadis_readdata($filename); + $xrds = Auth_Yadis_XRDS::parseXRDS($xml); + $this->_getCanonicalID($iname, $xrds, $expectedID); + } + } + + function _getCanonicalID($iname, $xrds, $expectedID) + { + if ($expectedID === null) { + $result = Auth_Yadis_getCanonicalID($iname, $xrds); + if ($result !== false) { + $this->fail($iname.' (got '.$result.')'); + } + } else { + $cid = Auth_Yadis_getCanonicalID($iname, $xrds); + $this->assertEquals(Auth_Yadis_XRI($expectedID), $cid); + } + } + + function test_services_filters() + { + // First, just be sure that service objects do the right + // thing. + $xml = Tests_Auth_Yadis_readdata("brian_priority.xrds"); + $xrds = Auth_Yadis_XRDS::parseXRDS($xml, + array('openid' => + 'http://openid.net/xmlns/1.0')); + $this->assertTrue($xrds !== null); + + // Get list of service objects. + $services = $xrds->services(); + $this->assertEquals(count($services), 2, "first service count"); + + // Query the two service objecs. + $s1 = $services[0]; + $this->assertEquals($s1->getPriority(), 1, "first priority check"); + $types = $s1->getTypes(); + $this->assertEquals(count($types), 1, "first type check"); + + $s2 = $services[1]; + $this->assertEquals($s2->getPriority(), 2, "second priority check"); + $types = $s2->getTypes(); + $this->assertEquals(count($types), 1, "second type check"); + + function _DelegateFilter(&$service) + { + if ($service->getElements('openid:Delegate')) { + return true; + } + return false; + } + + // Make sure that a filter which matches both DOES match both. + $this->assertEquals(count( + $xrds->services(array("_DelegateFilter"))), 2, + "_DelegateFilter check"); + + // This filter should match all services in the document. + function _HasTypeAndURI(&$service) + { + if ($service->getTypes() && + $service->getURIs()) { + return true; + } + return false; + } + + // This filter should only match one. + function _URIMatchesSchtuff(&$service) + { + $uris = $service->getURIs(); + + foreach ($uris as $uri) { + if (preg_match("|schtuff|", $uri)) { + return true; + } + } + return false; + } + + // This filter should only match one. + function _URIMatchesMyOpenID(&$service) + { + $uris = $service->getURIs(); + + foreach ($uris as $uri) { + if (preg_match("|myopenid|", $uri)) { + return true; + } + } + return false; + } + + // Make sure a pair of filters in ALL mode only match one service. + $this->assertEquals(count( + $xrds->services(array("_HasTypeAndURI", + "_URIMatchesSchtuff"), + SERVICES_YADIS_MATCH_ALL)), 1, + "_HasTypeAndURI / _URIMatchesSchtuff check"); + + // Make sure a pair of filters in ALL mode only match one service. + $this->assertEquals(count( + $xrds->services(array("_HasTypeAndURI", + "_URIMatchesMyOpenID"), + SERVICES_YADIS_MATCH_ALL)), 1, + "_HasTypeAndURI / _URIMatchesMyOpenID check"); + + // Make sure a pair of filters in ANY mode matches both services. + $this->assertEquals(count( + $xrds->services(array("_URIMatchesMyOpenID", + "_URIMatchesSchtuff"))), 2, + "_URIMatchesMyOpenID / _URIMatchesSchtuff check"); + + // Make sure the order of the services returned (when using + // filters) is correct. + $s = $xrds->services(array("_URIMatchesMyOpenID", + "_URIMatchesSchtuff")); + + $this->assertTrue($s[0]->getPriority() === 1, "s[0] priority check"); + $this->assertTrue($s[1]->getPriority() === 2, "s[1] priority check"); + + // Make sure a bad filter mode gets us a null service list. + $this->assertTrue($xrds->services(array("_URIMatchesMyOpenID", + "_URIMatchesSchtuff"), + "bogus") === null, + "bogus filter check"); + } + + function test_multisegment_xri() + { + $xml = Tests_Auth_Yadis_readdata('subsegments.xrds'); + $xmldoc = Auth_Yadis_XRDS::parseXRDS($xml); + $result = Auth_Yadis_getCanonicalId('xri://=nishitani*masaki', $xmldoc); + $this->assertEquals($result, "xri://=!E117.EF2F.454B.C707!0000.0000.3B9A.CA01"); + } +} + +?>
\ No newline at end of file diff --git a/Tests/Auth/Yadis/XRI.php b/Tests/Auth/Yadis/XRI.php new file mode 100644 index 0000000..8960d7a --- /dev/null +++ b/Tests/Auth/Yadis/XRI.php @@ -0,0 +1,144 @@ +<?php + +/** + * XRI resolution / handling tests. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once "PHPUnit.php"; +require_once "Auth/Yadis/XRIRes.php"; +require_once "Auth/Yadis/XRI.php"; +require_once "Auth/Yadis/Yadis.php"; + +class Tests_Auth_Yadis_XriDiscoveryTestCase extends PHPUnit_TestCase { + function runTest() + { + $this->assertEquals( + Auth_Yadis_identifierScheme('=john.smith'), 'XRI'); + + $this->assertEquals( + Auth_Yadis_identifierScheme(''), 'URI'); + + $this->assertEquals( + Auth_Yadis_identifierScheme('@smiths/john'), 'XRI'); + + $this->assertEquals( + Auth_Yadis_identifierScheme('smoker.myopenid.com'), 'URI'); + + $this->assertEquals( + Auth_Yadis_identifierScheme('xri://=john'), 'XRI'); + } +} + +class Tests_Auth_Yadis_XriEscapingTestCase extends PHPUnit_TestCase { + function test_escaping_percents() + { + $this->assertEquals(Auth_Yadis_escapeForIRI('@example/abc%2Fd/ef'), + '@example/abc%252Fd/ef'); + } + + function runTest() + { + // no escapes + $this->assertEquals('@example/foo/(@bar)', + Auth_Yadis_escapeForIRI('@example/foo/(@bar)')); + + // escape slashes + $this->assertEquals('@example/foo/(@bar%2Fbaz)', + Auth_Yadis_escapeForIRI('@example/foo/(@bar/baz)')); + + $this->assertEquals('@example/foo/(@bar%2Fbaz)/(+a%2Fb)', + Auth_Yadis_escapeForIRI('@example/foo/(@bar/baz)/(+a/b)')); + + // escape query ? and fragment + $this->assertEquals('@example/foo/(@baz%3Fp=q%23r)?i=j#k', + Auth_Yadis_escapeForIRI('@example/foo/(@baz?p=q#r)?i=j#k')); + } +} + +class Tests_Auth_Yadis_ProxyQueryTestCase extends PHPUnit_TestCase { + function setUp() + { + $this->proxy_url = 'http://xri.example.com/'; + $this->fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); + $this->proxy = new Auth_Yadis_ProxyResolver($fetcher, + $this->proxy_url); + $this->servicetype = 'xri://+i-service*(+forwarding)*($v*1.0)'; + $this->servicetype_enc = 'xri%3A%2F%2F%2Bi-service%2A%28%2Bforwarding%29%2A%28%24v%2A1.0%29'; + } + + function runTest() + { + $st = $this->servicetype; + $ste = $this->servicetype_enc; + $args_esc = "_xrd_r=application%2Fxrds%2Bxml&_xrd_t=" . $ste; + $h = $this->proxy_url; + $this->assertEquals($h . '=foo?' . $args_esc, + $this->proxy->queryURL('=foo', $st)); + $this->assertEquals($h . '=foo/bar?baz&' . $args_esc, + $this->proxy->queryURL('=foo/bar?baz', $st)); + $this->assertEquals($h . '=foo/bar?baz=quux&' . $args_esc, + $this->proxy->queryURL('=foo/bar?baz=quux', $st)); + $this->assertEquals($h . '=foo/bar?mi=fa&so=la&' . $args_esc, + $this->proxy->queryURL('=foo/bar?mi=fa&so=la', $st)); + + $args_esc = "_xrd_r=application%2Fxrds%2Bxml&_xrd_t=" . $ste; + $h = $this->proxy_url; + $this->assertEquals($h . '=foo/bar??' . $args_esc, + $this->proxy->queryURL('=foo/bar?', $st)); + $this->assertEquals($h . '=foo/bar????' . $args_esc, + $this->proxy->queryURL('=foo/bar???', $st)); + } +} + +class Tests_Auth_Yadis_TestGetRootAuthority extends PHPUnit_TestCase { + function runTest() + { + $xris = array( + array("@foo", "@"), + array("@foo*bar", "@"), + array("@*foo*bar", "@"), + array("@foo/bar", "@"), + array("!!990!991", "!"), + array("!1001!02", "!"), + array("=foo*bar", "="), + array("(example.com)/foo", "(example.com)"), + array("(example.com)*bar/foo", "(example.com)"), + array("baz.example.com/foo", "baz.example.com"), + array("baz.example.com:8080/foo", "baz.example.com:8080") + // Looking at the ABNF in XRI Syntax 2.0, I don't think you can + // have example.com*bar. You can do (example.com)*bar, but that + // would mean something else. + // ("example.com*bar/(=baz)", "example.com*bar"), + // ("baz.example.com!01/foo", "baz.example.com!01"), + ); + + foreach ($xris as $tupl) { + list($thexri, $expected_root) = $tupl; + $this->assertEquals(Auth_Yadis_XRI($expected_root), + Auth_Yadis_rootAuthority($thexri), + 'rootAuthority test ('.$thexri.')'); + } + } +} + +class Tests_Auth_Yadis_XRI extends PHPUnit_TestSuite { + function getName() + { + return "Tests_Auth_Yadis_XRI"; + } + + function Tests_Auth_Yadis_XRI() + { + $this->addTest(new Tests_Auth_Yadis_ProxyQueryTestCase()); + $this->addTest(new Tests_Auth_Yadis_XriEscapingTestCase()); + $this->addTest(new Tests_Auth_Yadis_XriDiscoveryTestCase()); + $this->addTest(new Tests_Auth_Yadis_TestGetRootAuthority()); + } +} + +?>
\ No newline at end of file diff --git a/Tests/Auth/Yadis/Yadis.php b/Tests/Auth/Yadis/Yadis.php new file mode 100644 index 0000000..837e447 --- /dev/null +++ b/Tests/Auth/Yadis/Yadis.php @@ -0,0 +1,90 @@ +<?php + +/** + * Tests for the core of the PHP Yadis library. + */ + +require_once 'PHPUnit.php'; +require_once 'Auth/Yadis/Yadis.php'; +require_once 'Tests/Auth/Yadis/TestUtil.php'; + +class Tests_Auth_Yadis_DiscoveryTest extends PHPUnit_TestCase { + + function Tests_Auth_Yadis_DiscoveryTest($input_url, $redir_uri, + $xrds_uri, $num) + { + $this->input_url = $input_url; + $this->redir_uri = $redir_uri; + $this->xrds_uri = $xrds_uri; + $this->num = $num; + } + + function getName() + { + return "Yadis discovery test ".$this->num; + } + + function runTest() + { + $fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); + $y = Auth_Yadis_Yadis::discover( + $this->input_url, $fetcher); + $this->assertTrue($y !== null); + + // Compare parts of returned Yadis object to expected URLs. + $this->assertEquals($this->redir_uri, $y->normalized_uri, "tried $this->input_url"); + + if ($this->xrds_uri) { + $this->assertEquals($this->xrds_uri, $y->xrds_uri); + // Compare contents of actual HTTP GET with that of Yadis + // response. + $f = Auth_Yadis_Yadis::getHTTPFetcher(); + $http_response = $f->get($this->xrds_uri); + + $this->assertEquals($http_response->body, $y->response_text); + } else { + $this->assertTrue($y->xrds_uri === null); + } + } +} + +class Tests_Auth_Yadis_Yadis extends PHPUnit_TestSuite { + + function getName() + { + return "Tests_Auth_Yadis_Yadis"; + } + + function parseTests($data) + { + $cases = explode("\n", $data); + $tests = array(); + + foreach ($cases as $line) { + if ($line && ($line[0] != "#")) { + $tests[] = explode("\t", $line, 3); + } + } + + return $tests; + } + + function Tests_Auth_Yadis_Yadis() + { + $test_data = file_get_contents('http://www.openidenabled.com/resources/yadis-test/discover/manifest.txt'); + + $test_cases = $this->parseTests($test_data); + + $i = 0; + foreach ($test_cases as $case) { + $i++; + list($input, $redir, $xrds) = $case; + $this->addTest(new Tests_Auth_Yadis_DiscoveryTest($input, + $redir, + $xrds, $i)); + } + } + +} + +?>
\ No newline at end of file diff --git a/Tests/Auth/Yadis/data/README b/Tests/Auth/Yadis/data/README new file mode 100644 index 0000000..3739cf1 --- /dev/null +++ b/Tests/Auth/Yadis/data/README @@ -0,0 +1,12 @@ +delegated-20060809.xrds - results from proxy.xri.net, determined by + Drummond and Kevin to be incorrect. +delegated-20060809-r1.xrds - Drummond's 1st correction +delegated-20060809-r2.xrds - Drummond's 2nd correction + +spoofs: keturn's (=!E4)'s attempts to log in with Drummond's i-number (=!D2) +spoof1.xrds +spoof2.xrds +spoof3.xrds - attempt to steal @!C0!D2 by having "at least one" CanonicalID + match the $res service ProviderID. + +ref.xrds - resolving @ootao*test.ref, which refers to a neustar XRI. diff --git a/Tests/Auth/Yadis/data/accept.txt b/Tests/Auth/Yadis/data/accept.txt new file mode 100644 index 0000000..0853321 --- /dev/null +++ b/Tests/Auth/Yadis/data/accept.txt @@ -0,0 +1,118 @@ +# Accept: [Accept: header value from RFC2616, +# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html] +# Available: [whitespace-separated content types] +# Expected: [Accept-header like list, containing the available content +# types with their q-values] + +Accept: */* +Available: text/plain +Expected: text/plain; q=1.0 + +Accept: */* +Available: text/plain, text/html +Expected: text/plain; q=1.0, text/html; q=1.0 + +# The order matters +Accept: */* +Available: text/html, text/plain +Expected: text/html; q=1.0, text/plain; q=1.0 + +Accept: text/*, */*; q=0.9 +Available: text/plain, image/jpeg +Expected: text/plain; q=1.0, image/jpeg; q=0.9 + +Accept: text/*, */*; q=0.9 +Available: image/jpeg, text/plain +Expected: text/plain; q=1.0, image/jpeg; q=0.9 + +# wildcard subtypes still reject differing main types +Accept: text/* +Available: image/jpeg, text/plain +Expected: text/plain; q=1.0 + +Accept: text/html +Available: text/html +Expected: text/html; q=1.0 + +Accept: text/html, text/* +Available: text/html +Expected: text/html; q=1.0 + +Accept: text/html, text/* +Available: text/plain, text/html +Expected: text/plain; q=1.0, text/html; q=1.0 + +Accept: text/html, text/*; q=0.9 +Available: text/plain, text/html +Expected: text/html; q=1.0, text/plain; q=0.9 + +# If a more specific type has a higher q-value, then the higher value wins +Accept: text/*; q=0.9, text/html +Available: text/plain, text/html +Expected: text/html; q=1.0, text/plain; q=0.9 + +Accept: */*, text/*; q=0.9, text/html; q=0.1 +Available: text/plain, text/html, image/monkeys +Expected: image/monkeys; q=1.0, text/plain; q=0.9, text/html; q=0.1 + +Accept: text/*, text/html; q=0 +Available: text/html +Expected: + +Accept: text/*, text/html; q=0 +Available: text/html, text/plain +Expected: text/plain; q=1.0 + +Accept: text/html +Available: text/plain +Expected: + +Accept: application/xrds+xml, text/html; q=0.9 +Available: application/xrds+xml, text/html +Expected: application/xrds+xml; q=1.0, text/html; q=0.9 + +Accept: application/xrds+xml, */*; q=0.9 +Available: application/xrds+xml, text/html +Expected: application/xrds+xml; q=1.0, text/html; q=0.9 + +Accept: application/xrds+xml, application/xhtml+xml; q=0.9, text/html; q=0.8, text/xml; q=0.7 +Available: application/xrds+xml, text/html +Expected: application/xrds+xml; q=1.0, text/html; q=0.8 + +# See http://www.rfc-editor.org/rfc/rfc3023.txt, section A.13 +Accept: application/xrds +Available: application/xrds+xml +Expected: + +Accept: application/xrds+xml +Available: application/xrds +Expected: + +Accept: application/xml +Available: application/xrds+xml +Expected: + +Available: application/xrds+xml +Accept: application/xml +Expected: + + + +################################################# +# The tests below this line are documentation of how this library +# works. If the implementation changes, it's acceptable to change the +# test to reflect that. These are specified so that we can make sure +# that the current implementation actually works the way that we +# expect it to given these inputs. + +Accept: text/html;level=1 +Available: text/html +Expected: text/html; q=1.0 + +Accept: text/html; level=1, text/html; level=9; q=0.1 +Available: text/html +Expected: text/html; q=1.0 + +Accept: text/html; level=9; q=0.1, text/html; level=1 +Available: text/html +Expected: text/html; q=1.0 diff --git a/Tests/Auth/Yadis/data/brian.multi.xrds b/Tests/Auth/Yadis/data/brian.multi.xrds new file mode 100644 index 0000000..1bc95de --- /dev/null +++ b/Tests/Auth/Yadis/data/brian.multi.xrds @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS + xmlns:xrds="xri://$xrds" + xmlns:openid="http://openid.net/xmlns/1.0" + xmlns="xri://$xrd*($v*2.0)"> + <XRD> + + <Service priority="2"> + <Type>http://openid.net/signon/1.1</Type> + <URI>http://www.myopenid.com/server</URI> + <openid:Delegate>http://frank.myopenid.com/</openid:Delegate> + </Service> + + </XRD> + <XRD> + + <Service priority="1"> + <Type>http://bar.com/</Type> + <URI>http://bar.com/server</URI> + </Service> + + <Service priority="2"> + <Type>http://foo.com</Type> + <URI>http://foo.com/server</URI> + </Service> + + </XRD> + <XRD> + + <Service priority="0"> + <Type>http://openid.net/signon/1.0</Type> + <URI>http://www.myopenid.com/server</URI> + <openid:Delegate>http://brian.myopenid.com/</openid:Delegate> + </Service> + + </XRD> +</xrds:XRDS> + diff --git a/Tests/Auth/Yadis/data/brian.multi_uri.xrds b/Tests/Auth/Yadis/data/brian.multi_uri.xrds new file mode 100644 index 0000000..fce5ef7 --- /dev/null +++ b/Tests/Auth/Yadis/data/brian.multi_uri.xrds @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS + xmlns:xrds="xri://$xrds" + xmlns:openid="http://openid.net/xmlns/1.0" + xmlns="xri://$xrd*($v*2.0)"> + <XRD> + + <Service> + <Type>http://openid.net/signon/1.0</Type> + <URI>http://www.myopenid.com/server</URI> + <URI>http://example.com/server</URI> + </Service> + + </XRD> +</xrds:XRDS> + diff --git a/Tests/Auth/Yadis/data/brian.xrds b/Tests/Auth/Yadis/data/brian.xrds new file mode 100644 index 0000000..c7539fe --- /dev/null +++ b/Tests/Auth/Yadis/data/brian.xrds @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS + xmlns:xrds="xri://$xrds" + xmlns:openid="http://openid.net/xmlns/1.0" + xmlns="xri://$xrd*($v*2.0)"> + <XRD> + + <Service priority="0"> + <Type>http://openid.net/signon/1.0</Type> + <URI>http://www.myopenid.com/server</URI> + <openid:Delegate>http://brian.myopenid.com/</openid:Delegate> + </Service> + + </XRD> +</xrds:XRDS> + diff --git a/Tests/Auth/Yadis/data/brian_priority.xrds b/Tests/Auth/Yadis/data/brian_priority.xrds new file mode 100644 index 0000000..273077d --- /dev/null +++ b/Tests/Auth/Yadis/data/brian_priority.xrds @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS + xmlns:xrds="xri://$xrds" + xmlns:openid="http://openid.net/xmlns/1.0" + xmlns="xri://$xrd*($v*2.0)"> + <XRD> + + <Service priority="2"> + <Type>http://openid.net/signon/1.0</Type> + <URI>http://www.schtuff.com/?action=openid_server</URI> + <openid:Delegate>http://users.schtuff.com/brian</openid:Delegate> + </Service> + + <Service priority="1"> + <Type>http://openid.net/signon/1.0</Type> + <URI>http://www.myopenid.com/server</URI> + <openid:Delegate>http://brian.myopenid.com/</openid:Delegate> + </Service> + + </XRD> +</xrds:XRDS> + diff --git a/Tests/Auth/Yadis/data/delegated-20060809-r1.xrds b/Tests/Auth/Yadis/data/delegated-20060809-r1.xrds new file mode 100644 index 0000000..f994b14 --- /dev/null +++ b/Tests/Auth/Yadis/data/delegated-20060809-r1.xrds @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<XRDS ref="xri://@ootao*test1" xmlns="xri://$xrds"> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*ootao</Query> + <Status code="100"/> + <Expires>2006-08-09T22:07:13.000Z</Expires> + <ProviderID>xri://@</ProviderID> + <LocalID priority="10">!5BAD.2AA.3C72.AF46</LocalID> + <CanonicalID priority="10">@!5BAD.2AA.3C72.AF46</CanonicalID> + <Service priority="10"> + <Type>xri://$res*auth*($v*2.0)</Type> + <ProviderID>xri://!!1003</ProviderID> + <MediaType>application/xrds+xml;trust=none</MediaType> + <URI priority="10">http://resolve.ezibroker.net/resolve/@ootao/</URI> + </Service> + <Service priority="10"> + <Type select="true">http://openid.net/signon/1.0</Type> + <ProviderID/> + <URI append="qxri" priority="1">https://linksafe.ezibroker.net/server/</URI> + </Service> + </XRD> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*test1</Query> + <Status code="100">SUCCESS</Status> + <ProviderID>xri://!!1003</ProviderID> + <LocalID>!0000.0000.3B9A.CA01</LocalID> + <CanonicalID>@!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01</CanonicalID> + <Service> + <Type select="true">http://openid.net/signon/1.0</Type> + <ProviderID/> + <URI append="qxri" priority="1">https://linksafe.ezibroker.net/server/</URI> + </Service> + </XRD> +</XRDS> diff --git a/Tests/Auth/Yadis/data/delegated-20060809-r2.xrds b/Tests/Auth/Yadis/data/delegated-20060809-r2.xrds new file mode 100644 index 0000000..68c08dc --- /dev/null +++ b/Tests/Auth/Yadis/data/delegated-20060809-r2.xrds @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<XRDS ref="xri://@ootao*test1" xmlns="xri://$xrds"> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*ootao</Query> + <Status code="100"/> + <Expires>2006-08-09T22:07:13.000Z</Expires> + <ProviderID>xri://@</ProviderID> + <LocalID priority="10">!5BAD.2AA.3C72.AF46</LocalID> + <CanonicalID priority="10">@!5BAD.2AA.3C72.AF46</CanonicalID> + <Service priority="10"> + <Type>xri://$res*auth*($v*2.0)</Type> + <ProviderID>xri://@!5BAD.2AA.3C72.AF46</ProviderID> + <MediaType>application/xrds+xml;trust=none</MediaType> + <URI priority="10">http://resolve.ezibroker.net/resolve/@ootao/</URI> + </Service> + <Service priority="10"> + <Type select="true">http://openid.net/signon/1.0</Type> + <ProviderID/> + <URI append="qxri" priority="1">https://linksafe.ezibroker.net/server/</URI> + </Service> + </XRD> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*test1</Query> + <Status code="100">SUCCESS</Status> + <ProviderID>xri://@!5BAD.2AA.3C72.AF46</ProviderID> + <LocalID>!0000.0000.3B9A.CA01</LocalID> + <CanonicalID>@!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01</CanonicalID> + <Service> + <Type select="true">http://openid.net/signon/1.0</Type> + <ProviderID/> + <URI append="qxri" priority="1">https://linksafe.ezibroker.net/server/</URI> + </Service> + </XRD> +</XRDS> diff --git a/Tests/Auth/Yadis/data/delegated-20060809.xrds b/Tests/Auth/Yadis/data/delegated-20060809.xrds new file mode 100644 index 0000000..073ee68 --- /dev/null +++ b/Tests/Auth/Yadis/data/delegated-20060809.xrds @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<XRDS ref="xri://@ootao*test1" xmlns="xri://$xrds"> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*ootao</Query> + <Status code="100"/> + <Expires>2006-08-09T22:07:13.000Z</Expires> + <ProviderID>xri://@</ProviderID> + <LocalID priority="10">!5BAD.2AA.3C72.AF46</LocalID> + <CanonicalID priority="10">@!5BAD.2AA.3C72.AF46</CanonicalID> + <Service priority="10"> + <Type>xri://$res*auth*($v*2.0)</Type> + <ProviderID/> + <MediaType>application/xrds+xml;trust=none</MediaType> + <URI priority="10">http://resolve.ezibroker.net/resolve/@ootao/</URI> + </Service> + <Service priority="10"> + <Type select="true">http://openid.net/signon/1.0</Type> + <ProviderID/> + <URI append="qxri" priority="1">https://linksafe.ezibroker.net/server/</URI> + </Service> + </XRD> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*test1</Query> + <Status code="100">SUCCESS</Status> + <ProviderID>xri://!!1003</ProviderID> + <LocalID>!0000.0000.3B9A.CA01</LocalID> + <CanonicalID>@!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01</CanonicalID> + <Service> + <Type select="true">http://openid.net/signon/1.0</Type> + <ProviderID/> + <URI append="qxri" priority="1">https://linksafe.ezibroker.net/server/</URI> + </Service> + </XRD> +</XRDS> diff --git a/Tests/Auth/Yadis/data/example-xrds.xml b/Tests/Auth/Yadis/data/example-xrds.xml new file mode 100644 index 0000000..101ba3b --- /dev/null +++ b/Tests/Auth/Yadis/data/example-xrds.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Sample XRDS file at: NAME --> +<xrds:XRDS + xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)"> + <XRD> + + <Service priority="0"> + <Type>http://example.com/</Type> + <URI>http://www.openidenabled.com/</URI> + </Service> + + </XRD> +</xrds:XRDS> diff --git a/Tests/Auth/Yadis/data/no-xrd.xml b/Tests/Auth/Yadis/data/no-xrd.xml new file mode 100644 index 0000000..ca66f73 --- /dev/null +++ b/Tests/Auth/Yadis/data/no-xrd.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS + xmlns:xrds="xri://$xrds" + xmlns:openid="http://openid.net/xmlns/1.0" + xmlns:typekey="http://typekey.com/xmlns/1.0" + xmlns="xri://$xrd*($v*2.0)"> +</xrds:XRDS> diff --git a/Tests/Auth/Yadis/data/not-xrds.xml b/Tests/Auth/Yadis/data/not-xrds.xml new file mode 100644 index 0000000..7f5bfd5 --- /dev/null +++ b/Tests/Auth/Yadis/data/not-xrds.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<x></x> diff --git a/Tests/Auth/Yadis/data/pip.xrds b/Tests/Auth/Yadis/data/pip.xrds new file mode 100644 index 0000000..ca271ab --- /dev/null +++ b/Tests/Auth/Yadis/data/pip.xrds @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS + xmlns:xrds="xri://$xrds" + xmlns:openid="http://openid.net/xmlns/1.0" + xmlns="xri://$xrd*($v*2.0)"> + <XRD> + + <Service priority="10"> + <Type>http://openid.net/signon/1.1</Type> + <Type>http://openid.net/sreg/1.0</Type> + <URI>https://pip.verisignlabs.com/server</URI> + </Service> + + <Service priority="20"> + <Type>http://openid.net/signon/1.0</Type> + <Type>http://openid.net/sreg/1.0</Type> + <URI>https://pip.verisignlabs.com/server</URI> + </Service> + + </XRD> +</xrds:XRDS> + diff --git a/Tests/Auth/Yadis/data/prefixsometimes.xrds b/Tests/Auth/Yadis/data/prefixsometimes.xrds new file mode 100644 index 0000000..5522a6e --- /dev/null +++ b/Tests/Auth/Yadis/data/prefixsometimes.xrds @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<XRDS ref="xri://@ootao*test1" xmlns="xri://$xrds"> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*ootao</Query> + <Status code="100"/> + <Expires>2006-08-09T22:07:13.000Z</Expires> + <ProviderID>xri://@</ProviderID> + <LocalID priority="10">!5BAD.2AA.3C72.AF46</LocalID> + <CanonicalID priority="10">@!5BAD.2AA.3C72.AF46</CanonicalID> + <Service priority="10"> + <Type>xri://$res*auth*($v*2.0)</Type> + <ProviderID>xri://@!5BAD.2AA.3C72.AF46</ProviderID> + <MediaType>application/xrds+xml;trust=none</MediaType> + <URI priority="10">http://resolve.ezibroker.net/resolve/@ootao/</URI> + </Service> + <Service priority="10"> + <Type select="true">http://openid.net/signon/1.0</Type> + <ProviderID/> + <URI append="qxri" priority="1">https://linksafe.ezibroker.net/server/</URI> + </Service> + </XRD> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*test1</Query> + <Status code="100">SUCCESS</Status> + <ProviderID>xri://@!5BAD.2AA.3C72.AF46</ProviderID> + <LocalID>!0000.0000.3B9A.CA01</LocalID> + <CanonicalID>xri://@!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01</CanonicalID> + <Service> + <Type select="true">http://openid.net/signon/1.0</Type> + <ProviderID/> + <URI append="qxri" priority="1">https://linksafe.ezibroker.net/server/</URI> + </Service> + </XRD> +</XRDS> diff --git a/Tests/Auth/Yadis/data/ref.xrds b/Tests/Auth/Yadis/data/ref.xrds new file mode 100644 index 0000000..69cf683 --- /dev/null +++ b/Tests/Auth/Yadis/data/ref.xrds @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="UTF-8"?> +<XRDS ref="xri://@ootao*test.ref" xmlns="xri://$xrds"> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*ootao</Query> + <Status code="100"/> + <Expires>2006-08-15T18:56:09.000Z</Expires> + <ProviderID>xri://@</ProviderID> + <LocalID priority="10">!5BAD.2AA.3C72.AF46</LocalID> + <CanonicalID priority="10">@!5BAD.2AA.3C72.AF46</CanonicalID> + <Service priority="10"> + <Type>xri://$res*auth*($v*2.0)</Type> + <ProviderID/> + <MediaType>application/xrds+xml;trust=none</MediaType> + <URI priority="10">http://resolve.ezibroker.net/resolve/@ootao/</URI> + </Service> + <Service priority="10"> + <Type select="true">http://openid.net/signon/1.0</Type> + <ProviderID/> + <URI append="qxri" priority="1">https://linksafe.ezibroker.net/server/</URI> + </Service> + </XRD> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*test.ref</Query> + <Status code="100">SUCCESS</Status> + <ProviderID>xri://!!1003</ProviderID> + <LocalID>!0000.0000.3B9A.CA03</LocalID> + <CanonicalID>@!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA03</CanonicalID> + <Ref>@!BAE.A650.823B.2475</Ref> + <Service> + <Type select="true">http://openid.net/signon/1.0</Type> + <ProviderID/> + <URI append="qxri" priority="1">https://linksafe.ezibroker.net/server/</URI> + </Service> + </XRD> + <XRDS ref="xri://@!BAE.A650.823B.2475" xmlns="xri://$xrds"> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>!BAE.A650.823B.2475</Query> + <Status code="100"/> + <Expires>2006-08-15T18:56:10.000Z</Expires> + <ProviderID>xri://@</ProviderID> + <LocalID priority="10">!BAE.A650.823B.2475</LocalID> + <CanonicalID priority="10">@!BAE.A650.823B.2475</CanonicalID> + <Service priority="10"> + <Type select="true">(+wdnc)</Type> + <ProviderID/> + <Path select="true">(+wdnc)</Path> + <URI append="none" priority="10">http://www.tcpacompliance.us</URI> + </Service> + <Service priority="10"> + <Type match="content" select="true">xri://$res*auth*($v*2.0)</Type> + <ProviderID/> + <MediaType match="content" select="false">application/xrds+xml;trust=none</MediaType> + <URI priority="10">http://dev.dready.org/cgi-bin/xri</URI> + </Service> + <Service priority="10"> + <Type match="content" select="true">(+i-name)</Type> + <ProviderID/> + <Path match="content" select="true">(+i-name)</Path> + <URI append="none" priority="10">http://www.inames.net</URI> + </Service> + <Service priority="10"> + <Type select="true">xri://+i-service*(+contact)*($v*1.0)</Type> + <Type match="default" select="false"/> + <ProviderID>xri://!!1001</ProviderID> + <Path select="true">(+contact)</Path> + <Path match="null" select="false"/> + <MediaType select="false">text/html</MediaType> + <MediaType match="default" select="false"/> + <URI append="none" priority="10">http://www.neustar.biz</URI> + </Service> + </XRD> + </XRDS> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>!BAE.A650.823B.2475</Query> + <Status code="100"/> + <Expires>2006-08-15T18:56:10.000Z</Expires> + <ProviderID>xri://@</ProviderID> + <LocalID priority="10">!BAE.A650.823B.2475</LocalID> + <CanonicalID priority="10">@!BAE.A650.823B.2475</CanonicalID> + <Service priority="10"> + <Type select="true">(+wdnc)</Type> + <ProviderID/> + <Path select="true">(+wdnc)</Path> + <URI append="none" priority="10">http://www.tcpacompliance.us</URI> + </Service> + <Service priority="10"> + <Type match="content" select="true">xri://$res*auth*($v*2.0)</Type> + <ProviderID/> + <MediaType match="content" select="false">application/xrds+xml;trust=none</MediaType> + <URI priority="10">http://dev.dready.org/cgi-bin/xri</URI> + </Service> + <Service priority="10"> + <Type match="content" select="true">(+i-name)</Type> + <ProviderID/> + <Path match="content" select="true">(+i-name)</Path> + <URI append="none" priority="10">http://www.inames.net</URI> + </Service> + <Service priority="10"> + <Type select="true">xri://+i-service*(+contact)*($v*1.0)</Type> + <Type match="default" select="false"/> + <ProviderID>xri://!!1001</ProviderID> + <Path select="true">(+contact)</Path> + <Path match="null" select="false"/> + <MediaType select="false">text/html</MediaType> + <MediaType match="default" select="false"/> + <URI append="none" priority="10">http://www.neustar.biz</URI> + </Service> + </XRD> +</XRDS>
\ No newline at end of file diff --git a/Tests/Auth/Yadis/data/sometimesprefix.xrds b/Tests/Auth/Yadis/data/sometimesprefix.xrds new file mode 100644 index 0000000..eff7555 --- /dev/null +++ b/Tests/Auth/Yadis/data/sometimesprefix.xrds @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<XRDS ref="xri://@ootao*test1" xmlns="xri://$xrds"> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*ootao</Query> + <Status code="100"/> + <Expires>2006-08-09T22:07:13.000Z</Expires> + <ProviderID>xri://@</ProviderID> + <LocalID priority="10">!5BAD.2AA.3C72.AF46</LocalID> + <CanonicalID priority="10">xri://@!5BAD.2AA.3C72.AF46</CanonicalID> + <Service priority="10"> + <Type>xri://$res*auth*($v*2.0)</Type> + <ProviderID>xri://@!5BAD.2AA.3C72.AF46</ProviderID> + <MediaType>application/xrds+xml;trust=none</MediaType> + <URI priority="10">http://resolve.ezibroker.net/resolve/@ootao/</URI> + </Service> + <Service priority="10"> + <Type select="true">http://openid.net/signon/1.0</Type> + <ProviderID/> + <URI append="qxri" priority="1">https://linksafe.ezibroker.net/server/</URI> + </Service> + </XRD> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*test1</Query> + <Status code="100">SUCCESS</Status> + <ProviderID>xri://@!5BAD.2AA.3C72.AF46</ProviderID> + <LocalID>!0000.0000.3B9A.CA01</LocalID> + <CanonicalID>@!5BAD.2AA.3C72.AF46!0000.0000.3B9A.CA01</CanonicalID> + <Service> + <Type select="true">http://openid.net/signon/1.0</Type> + <ProviderID/> + <URI append="qxri" priority="1">https://linksafe.ezibroker.net/server/</URI> + </Service> + </XRD> +</XRDS> diff --git a/Tests/Auth/Yadis/data/spoof1.xrds b/Tests/Auth/Yadis/data/spoof1.xrds new file mode 100644 index 0000000..8e870c8 --- /dev/null +++ b/Tests/Auth/Yadis/data/spoof1.xrds @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<XRDS ref="xri://=keturn*isDrummond" xmlns="xri://$xrds"> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*keturn</Query> + <ProviderID>xri://=</ProviderID> + <LocalID>!E4</LocalID> + <CanonicalID>=!E4</CanonicalID> + + <Service> + <Type>xri://$res*auth*($v*2.0)</Type> + <URI>http://keturn.example.com/resolve/</URI> + <ProviderID>=!E4</ProviderID> + </Service> + </XRD> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*isDrummond</Query> + <ProviderID>=!E4</ProviderID> + <LocalID>!D2</LocalID> + <CanonicalID>=!D2</CanonicalID> + <Service> + <Type>http://openid.net/signon/1.0</Type> + <URI>http://keturn.example.com/openid</URI> + </Service> + </XRD> +</XRDS> diff --git a/Tests/Auth/Yadis/data/spoof2.xrds b/Tests/Auth/Yadis/data/spoof2.xrds new file mode 100644 index 0000000..7547561 --- /dev/null +++ b/Tests/Auth/Yadis/data/spoof2.xrds @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<XRDS ref="xri://=keturn*isDrummond" xmlns="xri://$xrds"> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*keturn</Query> + <ProviderID>xri://=</ProviderID> + <LocalID>!E4</LocalID> + <CanonicalID>=!E4</CanonicalID> + + <Service> + <Type>xri://$res*auth*($v*2.0)</Type> + <URI>http://keturn.example.com/resolve/</URI> + <ProviderID>xri://=</ProviderID> + </Service> + </XRD> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*isDrummond</Query> + <ProviderID>xri://=</ProviderID> + <LocalID>!D2</LocalID> + <CanonicalID>=!D2</CanonicalID> + <Service> + <Type>http://openid.net/signon/1.0</Type> + <URI>http://keturn.example.com/openid</URI> + </Service> + </XRD> +</XRDS> diff --git a/Tests/Auth/Yadis/data/spoof3.xrds b/Tests/Auth/Yadis/data/spoof3.xrds new file mode 100644 index 0000000..f4c43c9 --- /dev/null +++ b/Tests/Auth/Yadis/data/spoof3.xrds @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<XRDS ref="xri://=keturn*isDrummond" xmlns="xri://$xrds"> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*keturn</Query> + <ProviderID>xri://@</ProviderID> + <LocalID>@E4</LocalID> + <CanonicalID>@!E4</CanonicalID> + + <Service> + <Type>xri://$res*auth*($v*2.0)</Type> + <URI>http://keturn.example.com/resolve/</URI> + <ProviderID>@!E4</ProviderID> + </Service> + </XRD> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*is</Query> + <ProviderID>@!E4</ProviderID> + <LocalID>!D2</LocalID> + <CanonicalID>=!C0</CanonicalID> + <CanonicalID>=!E4!01</CanonicalID> + <Service> + <Type>xri://$res*auth*($v*2.0)</Type> + <URI>http://keturn.example.com/resolve/</URI> + <ProviderID>@!C0</ProviderID> + </Service> + </XRD> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*drummond</Query> + <ProviderID>@!C0</ProviderID> + <LocalID>!D2</LocalID> + <CanonicalID>@!C0!D2</CanonicalID> + <Service> + <Type>http://openid.net/signon/1.0</Type> + <URI>http://keturn.example.com/openid</URI> + </Service> + </XRD> +</XRDS> diff --git a/Tests/Auth/Yadis/data/subsegments.xrds b/Tests/Auth/Yadis/data/subsegments.xrds new file mode 100644 index 0000000..11d2e91 --- /dev/null +++ b/Tests/Auth/Yadis/data/subsegments.xrds @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<XRDS ref="xri://=nishitani*masaki" xmlns="xri://$xrds"> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*nishitani</Query> + <Status code="100"/> + <Expires>2007-12-25T11:33:39.000Z</Expires> + <ProviderID>xri://=</ProviderID> + <LocalID priority="10">!E117.EF2F.454B.C707</LocalID> + <CanonicalID priority="10">=!E117.EF2F.454B.C707</CanonicalID> + <Service priority="10"> + <Type select="true">http://openid.net/signon/1.0</Type> + <ProviderID>xri://!!1003!103</ProviderID> + <URI append="none" priority="1">https://linksafe.ezibroker.net/server/</URI> + </Service> + <Service priority="10"> + <Type select="true">xri://$res*auth*($v*2.0)</Type> + <ProviderID>xri://!!1003!103</ProviderID> + <MediaType>application/xrds+xml;trust=none</MediaType> + <URI priority="10">http://resolve.ezibroker.net/resolve/=nishitani/</URI> + </Service> + <Service priority="1"> + <Type match="content" select="true">xri://+i-service*(+forwarding)*($v*1.0)</Type> + <Type match="null" select="false"/> + <ProviderID>xri://!!1003!103</ProviderID> + <Path match="content">(+index)</Path> + <Path match="default"/> + <URI append="qxri" priority="1">http://linksafe-forward.ezibroker.net/forwarding/</URI> + </Service> + </XRD> + <XRD xmlns="xri://$xrd*($v*2.0)"> + <Query>*masaki</Query> + <Status code="100">SUCCESS</Status> + <ProviderID>xri://!!1003</ProviderID> + <LocalID>!0000.0000.3B9A.CA01</LocalID> + <CanonicalID>=!E117.EF2F.454B.C707!0000.0000.3B9A.CA01</CanonicalID> + <Service> + <Type select="true">http://openid.net/signon/1.0</Type> + <ProviderID>xri://!!1003!103</ProviderID> + <URI append="none" priority="1">https://linksafe.ezibroker.net/server/</URI> + </Service> + <Service> + <Type select="true">xri://+i-service*(+contact)*($v*1.0)</Type> + <Type match="null"/> + <ProviderID>xri://!!1003!103</ProviderID> + <Path select="true">(+contact)</Path> + <Path match="null"/> + <URI append="authority" priority="1">http://linksafe-contact.ezibroker.net/contact/</URI> + </Service> + <Service priority="1"> + <Type match="content" select="true">xri://+i-service*(+forwarding)*($v*1.0)</Type> + <Type match="null" select="false"/> + <ProviderID>xri://!!1003!103</ProviderID> + <Path match="content">(+index)</Path> + <Path match="default"/> + <URI append="qxri" priority="1">http://linksafe-forward.ezibroker.net/forwarding/</URI> + </Service> + </XRD> +</XRDS> diff --git a/Tests/Auth/Yadis/data/test1-discover.txt b/Tests/Auth/Yadis/data/test1-discover.txt new file mode 100644 index 0000000..7ec9b87 --- /dev/null +++ b/Tests/Auth/Yadis/data/test1-discover.txt @@ -0,0 +1,137 @@ +equiv +Status: 200 OK +Content-Type: text/html + +<html> +<head> +<meta http-equiv="YADIS_HEADER" content="URL_BASE/xrds"> +<title>Joe Schmoe's Homepage</title> +</head> +<body> +<h1>Joe Schmoe's Homepage</h1> +<p>Blah blah blah blah blah blah blah</p> +</body> +</html> + +header +Status: 200 OK +Content-Type: text/html +YADIS_HEADER: URL_BASE/xrds + +<html> +<head> +<title>Joe Schmoe's Homepage</title> +</head> +<body> +<h1>Joe Schmoe's Homepage</h1> +<p>Blah blah blah blah blah blah blah</p> +</body> + +xrds +Status: 200 OK +Content-Type: application/xrds+xml + +<XRDS Content> + +xrds_ctparam +Status: 200 OK +Content-Type: application/xrds+xml; charset=UTF8 + +<XRDS Content> + +xrds_ctcase +Status: 200 OK +Content-Type: appliCATION/XRDS+xml + +<XRDS Content> + +xrds_html +Status: 200 OK +Content-Type: text/html + +<XRDS Content> + +redir_equiv +Status: 302 Found +Content-Type: text/plain +Location: URL_BASE/equiv + +You are presently being redirected. + +redir_header +Status: 302 Found +Content-Type: text/plain +Location: URL_BASE/header + +You are presently being redirected. + +redir_xrds +Status: 302 Found +Content-Type: application/xrds+xml +Location: URL_BASE/xrds + +<XRDS Content> + +redir_xrds_html +Status: 302 Found +Content-Type: text/plain +Location: URL_BASE/xrds_html + +You are presently being redirected. + +redir_redir_equiv +Status: 302 Found +Content-Type: text/plain +Location: URL_BASE/redir_equiv + +You are presently being redirected. + +lowercase_header +Status: 200 OK +Content-Type: text/html +x-xrds-location: URL_BASE/xrds + +<html> +<head> +<title>Joe Schmoe's Homepage</title> +</head> +<body> +<h1>Joe Schmoe's Homepage</h1> +<p>Blah blah blah blah blah blah blah</p> +</body> + +404_server_response +Status: 404 Not Found + +EEk! + +500_server_response +Status: 500 Server error + +EEk! + +201_server_response +Status: 201 Created + +EEk! + +404_with_header +Status: 404 Not Found +YADIS_HEADER: URL_BASE/xrds + +EEk! + +404_with_meta +Status: 404 Not Found +Content-Type: text/html + +<html> +<head> +<meta http-equiv="YADIS_HEADER" content="URL_BASE/xrds"> +<title>Joe Schmoe's Homepage</title> +</head> +<body> +<h1>Joe Schmoe's Homepage</h1> +<p>Blah blah blah blah blah blah blah</p> +</body> +</html> diff --git a/Tests/Auth/Yadis/data/test1-parsehtml.txt b/Tests/Auth/Yadis/data/test1-parsehtml.txt new file mode 100644 index 0000000..752ad09 --- /dev/null +++ b/Tests/Auth/Yadis/data/test1-parsehtml.txt @@ -0,0 +1,149 @@ +found +<!-- minimal well-formed success case --> +<html><head><meta http-equiv="X-XRDS-Location" content="found"></head></html> + +found +<!-- minimal well-formed success case, xhtml closing, whitespace --> +<html><head><meta http-equiv="X-XRDS-Location" content="found" /></head></html> + +found +<!-- minimal well-formed success case, xhtml closing, no whitespace --> +<html><head><meta http-equiv="X-XRDS-Location" content="found"/></head></html> + +found +<!-- minimal success case --> +<html><head><meta http-equiv="X-XRDS-Location" content="found"> + +found +<!-- ignore bogus top-level tags --> +</porky><html><head><meta http-equiv="X-XRDS-Location" content="found"> + +found +<!-- Case folding for header name --> +<html><head><meta http-equiv="x-xrds-location" content="found"> + +found +<!-- missing <html> tag --> +<head><meta http-equiv="X-XRDS-Location" content="found"> + +found +<!-- javascript in head --> +<html><head><script type="text/javascript">document.write("<body>");</script><META http-equiv="X-XRDS-Location" content="found"> + +EOF +<!-- javascript in head --> +<html><head><script type="text/javascript">document.write("<body>");<META http-equiv="X-XRDS-Location" content="found"> + +found +<!-- case folding for tag names --> +<html><head><META http-equiv="X-XRDS-Location" content="found"> + +found +<!-- Stop after first one found --> +<html><head> +<meta http-equiv="x-xrds-location" content="found"> +<meta http-equiv="x-xrds-location" content="not-found"> + +& +<!-- standard entity --> +<head><meta http-equiv="X-XRDS-Location" content="&"> + +found +<!-- hex entity --> +<html> + <head> + <meta http-equiv="X-XRDS-Location" content="found"> + </head> +</html> + +found +<!-- decimal entity --> +<html> + <head> + <meta http-equiv="X-XRDS-Location" content="found"> + </head> +</html> + +/ +<!-- hex entity --> +<html> + <head> + <meta http-equiv="X-XRDS-Location" content="/"> + </head> +</html> + + +<!-- empty string --> +<html><head><meta http-equiv="X-XRDS-Location" content=""> + +EOF +<!-- No markup, except this comment --> + +None +<!-- No meta, just standard HTML --> +<html> + <head> + <title>A boring document</title> + </head> + <body> + <h1>A boring document</h1> + <p>There's really nothing interesting about this</p> + </body> +</html> + +EOF +<!-- No <html> or <head> --> +<meta http-equiv="X-XRDS-Location" content="found"> + +EOF +<!-- No <head> tag --> +<html><meta http-equiv="X-XRDS-Location" content="found"> + +None +<!-- No <html> or <head> and a <body> --> +<body><meta http-equiv="X-XRDS-Location" content="found"> + +None +<!-- <head> and <html> reversed --> +<head><html><meta http-equiv="X-XRDS-Location" content="found"> + +None +<!-- <meta> is inside of <body> --> +<html><head><body><meta http-equiv="X-XRDS-Location" content="found"> + +None +<!-- <meta> is inside comment --> +<html> + <head> + <!--<meta http-equiv="X-XRDS-Location" content="found">--> + </head> +</html> + +None +<!-- <meta> is inside of <body> --> +<html> + <head> + <title>Someone's blog</title> + </head> + <body> + <h1>My blog</h1> + <p>This is my blog</p> + <h2>Comments</h2> + <p><meta http-equiv="X-XRDS-Location" content="found"></p> + </body> +</html> + +None +<!-- short head tag --> +<html><head/> +<meta http-equiv="X-XRDS-Location" content="found"> + +None +<!-- <body> comes first --> +<body><html><head> +<meta http-equiv="X-XRDS-Location" content="found"> + +None +<!-- </body> comes first --> +</body><html><head> +<meta http-equiv="X-XRDS-Location" content="found"> diff --git a/Tests/Auth/Yadis/data/test1-xrd.xml b/Tests/Auth/Yadis/data/test1-xrd.xml new file mode 100644 index 0000000..60e5ca7 --- /dev/null +++ b/Tests/Auth/Yadis/data/test1-xrd.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS + xmlns:xrds="xri://$xrds" + xmlns:openid="http://openid.net/xmlns/1.0" + xmlns:typekey="http://typekey.com/xmlns/1.0" + xmlns="xri://$xrd*($v*2.0)"> + <XRD> + + <Service priority="0"> + <Type>http://openid.net/signon/1.0</Type> + <URI>http://www.myopenid.com/server</URI> + <openid:Delegate>http://josh.myopenid.com/</openid:Delegate> + </Service> + + <Service priority="20"> + <Type>http://lid.netmesh.org/sso/2.0b5</Type> + <Type>http://lid.netmesh.org/2.0b5</Type> + <URI>http://mylid.net/josh</URI> + </Service> + + <Service priority="10"> + <Type>http://openid.net/signon/1.0</Type> + <URI>http://www.livejournal.com/openid/server.bml</URI> + <openid:Delegate>http://www.livejournal.com/users/nedthealpaca/</openid:Delegate> + </Service> + + <Service priority="15"> + <Type>http://typekey.com/services/1.0</Type> + <typekey:MemberName>joshhoyt</typekey:MemberName> + </Service> + + <Service priority="5"> + <Type>http://openid.net/signon/1.0</Type> + <URI>http://www.schtuff.com/openid</URI> + <openid:Delegate>http://users.schtuff.com/josh</openid:Delegate> + </Service> + + </XRD> +</xrds:XRDS> diff --git a/Tests/Auth/Yadis/data/uri_priority.xrds b/Tests/Auth/Yadis/data/uri_priority.xrds new file mode 100644 index 0000000..b1a2f65 --- /dev/null +++ b/Tests/Auth/Yadis/data/uri_priority.xrds @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS + xmlns:xrds="xri://$xrds" + xmlns:openid="http://openid.net/xmlns/1.0" + xmlns="xri://$xrd*($v*2.0)"> + <XRD> + + <Service> + <Type>http://openid.net/signon/1.0</Type> + <URI>http://no.priority/</URI> + <URI priority="1">http://one.priority/</URI> + <URI priority="0">http://zero.priority/</URI> + </Service> + + </XRD> +</xrds:XRDS> diff --git a/Tests/TestDriver.php b/Tests/TestDriver.php new file mode 100644 index 0000000..a19b502 --- /dev/null +++ b/Tests/TestDriver.php @@ -0,0 +1,202 @@ +<?php + +/** + * A driver for the PHP OpenID unit tests. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +require_once 'PHPUnit.php'; +require_once 'PHPUnit/GUI/HTML.php'; + +error_reporting(E_ALL); + +global $__test_errors; +$__test_errors = array(); + +function __handler($code, $message) +{ + global $__test_errors; + + if ($code == E_USER_WARNING) { + $__test_errors[] = $message; + } +} + +function __raiseError($message) +{ + set_error_handler('__handler'); + trigger_error($message, E_USER_WARNING); + restore_error_handler(); +} + +function __getError() +{ + global $__test_errors; + if ($__test_errors) { + return array_pop($__test_errors); + } + return null; +} + +/** + * Load the tests that are defined in the named modules. + * + * If you have Tests/Foo.php which defines a test class called + * Tests_Foo, the call would look like: + * + * loadTests('Tests/', array('Foo')) + * + * @param string $test_dir The root of the test hierarchy. Must end + * with a / + * + * @param array $test_names The names of the modules in which the + * tests are defined. This should not include the root of the test + * hierarchy. + */ +function loadTests($test_dir, $test_names) +{ + global $_tests; + $suites = array(); + + foreach ($test_names as $filename) { + $filename = $test_dir . $filename . '.php'; + + if (!global_require_once($filename)) { + continue; + } + + $class_name = str_replace('/', '_', $filename); + $class_name = basename($class_name, '.php'); + + $suites[] = makeSuite($class_name); + } + + return $suites; +} + +function makeSuite($class_name) { + $test = new $class_name($class_name); + + if (is_a($test, 'PHPUnit_TestCase')) { + $s = new PHPUnit_TestSuite(); + $s->setName($class_name); + $s->addTestSuite($class_name); + $test = $s; + } + + $tc_array_name = $class_name . '_other'; + if (array_key_exists($tc_array_name, $GLOBALS) && + is_array($GLOBALS[$tc_array_name])) { + + foreach ($GLOBALS[$tc_array_name] as $tc) { + $test->addTestSuite(get_class($tc)); + } + } + + return $test; +} + + +function global_require_once($name) +{ + $f = include_once $name; + if (!$f) { + print("global require once skipping $name\n"); + return false; + } + foreach (get_defined_vars() as $k => $v) { + if (!in_array($k, array('name', 'GLOBALS'))) { + $GLOBALS[$k] = $v; + } + } + return true; +} + +$_tests = array( + array( + 'dir' => 'Tests/Auth/OpenID/', + 'files' => array( + 'Association', + 'AssociationResponse', + 'AuthRequest', + 'AX', + 'BigMath', + 'Consumer', + 'CryptUtil', + 'DiffieHellman', + 'Discover_OpenID', + 'Extension', + 'HMAC', + 'KVForm', + 'Message', + 'Negotiation', + 'Nonce', + 'OpenID_Yadis', + 'PAPE', + 'Parse', + 'RPVerify', + 'Server', + 'SReg', + 'StoreTest', + 'TrustRoot', + 'URINorm', + 'Util', + 'VerifyDisco'), + ), + array( + 'dir' => 'Tests/Auth/Yadis/', + 'files' => array( + 'ParseHTML', + 'XRDS', + 'Yadis', + 'Discover_Yadis', + 'XRI' + ) + ) + ); + +function selectTests($package, $names) +{ + global $_tests; + $lnames = array_map('strtolower', $names); + $include = array(); + $exclude = array(); + foreach ($package['files'] as $t) { + $l = strtolower($t); + if (in_array($l, $lnames)) { + $include[] = $t; + } + + if (in_array("/$l", $lnames)) { + $exclude[] = $t; + } + } + + return array_diff($include, $exclude); +} + +// Load OpenID library tests +function loadSuite($names=null) +{ + global $_tests; + $result = array(); + foreach ($_tests as $package) { + if (!$names) { + $selected = $package['files']; + } else { + $selected = selectTests($package, $names); + } + $result = array_merge($result, loadTests($package['dir'], $selected)); + } + + return $result; +} +?> diff --git a/admin/adminutil.php b/admin/adminutil.php new file mode 100644 index 0000000..7389838 --- /dev/null +++ b/admin/adminutil.php @@ -0,0 +1,30 @@ +<?php + +/** + * Add a directory to the include path + * + * @param dir: The directory to add to the path + * @param at_start: If true, place this directory at the beginning of + * the include path. Otherwise, place it at the end. + */ +function includeAdd($dir, $at_start=false) +{ + $path = ini_get('include_path'); + if (strlen($path)) { + $newpath = $at_start ? "$dir:$path" : "$path:$dir"; + } else { + $newpath = $dir; + } + + ini_set('include_path', $newpath); +} + +/** + * Return the parent directory of this module. + */ +function getParent() +{ + return dirname(dirname(realpath(__FILE__))); +} + +?>
\ No newline at end of file diff --git a/admin/brace_style.pl b/admin/brace_style.pl new file mode 100644 index 0000000..ed8332f --- /dev/null +++ b/admin/brace_style.pl @@ -0,0 +1,81 @@ +#!/usr/bin/env perl -w + +use strict; + +my $filename = $ARGV[0]; + +if (!$filename) { + print "Usage: modified_otb.pl <filename>\n"; + exit(1); +} + +my @results = (); +my $line_num = 0; +my ($NONE, $BRACE, $PAREN) = (0, 1, 2); +my $looking_for = $NONE; +my $last_func_name = ""; + +open(HANDLE, "<", $filename) or die "Cannot open $filename\n"; + +# Read the file and track the lines with length > $max_length. +while (<HANDLE>) { + $line_num++; + # Subtract one because the newline doesn't count toward the + # length. + chomp; + + if (!$looking_for && + ($_ =~ /^\s*function/) && + ($_ =~ /\{/)) { + # Done (bad): we found a function whose opening line ends with + # a brace, which goes against the PEAR coding guidelines. + + ($last_func_name) = $_ =~ /function\s*(.*)\(/; + + push @results, "'$last_func_name' prototype ends with opening ". + "brace, line $line_num"; + } elsif (!$looking_for && + ($_ =~ /^\s*function/) && + ($_ !~ /\)/)) { + ($last_func_name) = $_ =~ /function\s*(.*)\(/; + $looking_for = $PAREN; + } elsif (($looking_for == $PAREN) && + ($_ =~ /\)/) && + ($_ =~ /\{/)) { + # Done (bad): function prototype and brace are on the same + # line. + push @results, "'$last_func_name' prototype ends with with ". + "opening brace, line $line_num"; + $looking_for = $NONE; + } elsif (($looking_for == $PAREN) && + ($_ =~ /\)/) && + ($_ !~ /\{/)) { + $looking_for = $BRACE; + } elsif (!$looking_for && + ($_ =~ /^\s*function/) && + ($_ =~ /\)/) && + ($_ !~ /\{/)) { + ($last_func_name) = $_ =~ /function\s*(.*)\(/; + $looking_for = $BRACE; + } elsif (($looking_for == $BRACE) && + ($_ eq "{")) { + $looking_for = $NONE; + # Done (good): the brace was found on the line after the + # function prototype. + } else { + # We got here because we got a line that we're not interested + # in. + $looking_for = $NONE; + } +} + +# If any long lines were found, notify and exit(1); otherwise, +# exit(0). +if (@results) { + foreach my $result (@results) { + print "$filename: $result\n"; + } + exit(1); +} else { + exit(0); +} diff --git a/admin/checkimport b/admin/checkimport new file mode 100644 index 0000000..05776d8 --- /dev/null +++ b/admin/checkimport @@ -0,0 +1,4 @@ +#!/usr/bin/env php +<?php +require_once $argv[1]; +?>
\ No newline at end of file diff --git a/admin/checkimports b/admin/checkimports new file mode 100644 index 0000000..c4fa471 --- /dev/null +++ b/admin/checkimports @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +./admin/findphp | xargs -L 1 ./admin/checkimport
\ No newline at end of file diff --git a/admin/darcs-ignore b/admin/darcs-ignore new file mode 100644 index 0000000..8291ce2 --- /dev/null +++ b/admin/darcs-ignore @@ -0,0 +1,3 @@ +~$ +^doc(/|$) +^CHANGELOG$ diff --git a/admin/docblocks b/admin/docblocks new file mode 100644 index 0000000..a7f05c3 --- /dev/null +++ b/admin/docblocks @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +#set -e + +bad_files=$(./admin/findphp | xargs -L 1 /usr/bin/env perl admin/docblocks.pl) + +if [ "$bad_files" ] + then + cat <<EOF 1>&2 +These files do not start with docblocks: + +$bad_files + +EOF + exit 1 +fi diff --git a/admin/docblocks.pl b/admin/docblocks.pl new file mode 100644 index 0000000..0483dbb --- /dev/null +++ b/admin/docblocks.pl @@ -0,0 +1,26 @@ +#!/usr/bin/env perl -w + +use strict; + +my $filename = $ARGV[0]; + +if (!$filename) { + print "Usage: docblocks.pl <filename>\n"; + exit(1); +} + +my %allowed = ("" => 1, + "<?php" => 1); + +open(HANDLE, "<", $filename) or die "Cannot open $filename\n"; + +while (<HANDLE>) { + chomp; + + if ($_ =~ /\/\*\*/) { + exit(0); + } elsif (!$allowed{$_}) { + print $filename."\n"; + exit(1); + } +} diff --git a/admin/findallphp b/admin/findallphp new file mode 100644 index 0000000..59a7306 --- /dev/null +++ b/admin/findallphp @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +find Auth Tests \ + -name _darcs -prune -o \ + \( -type f \ + -a -name \*.php \ + -a ! -name .\* \ + \) diff --git a/admin/findglobals b/admin/findglobals new file mode 100644 index 0000000..2fcb0e6 --- /dev/null +++ b/admin/findglobals @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +# Find all PHP modules that are likely to have global variables +set -e + +./admin/findphp | xargs grep '^\$' diff --git a/admin/findphp b/admin/findphp new file mode 100644 index 0000000..d529af7 --- /dev/null +++ b/admin/findphp @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +find Auth Tests \ + -name _darcs -prune -o \ + \( -type f \ + -a -name \*.php \ + -a ! -name .\* \ + \) | grep -v Tests diff --git a/admin/fixperms b/admin/fixperms new file mode 100644 index 0000000..0ea0944 --- /dev/null +++ b/admin/fixperms @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +cat <<EOF | xargs chmod +x +admin/checkimport +admin/checkimports +admin/docblocks +admin/findphp +admin/findglobals +admin/fixperms +admin/makedoc.sh +admin/mathlib +admin/prepare-release +admin/runtests +admin/findallphp +admin/syntaxcheck +EOF diff --git a/admin/gettlds.py b/admin/gettlds.py new file mode 100644 index 0000000..4300638 --- /dev/null +++ b/admin/gettlds.py @@ -0,0 +1,47 @@ +""" +Fetch the current TLD list from the IANA Web site, parse it, and print +an expression suitable for direct insertion into each library's trust +root validation module + +Usage: + python gettlds.py (php|python|ruby) + +Then cut-n-paste. +""" + +import urllib2 + +import sys + +langs = { + 'php': (r"'/\.(", + "'", "|", "|' .", + r")\.?$/'"), + 'python': ("['", + "'", "', '", "',", + "']"), + 'ruby': ("%w'", + "", " ", "", + "'"), + } + +lang = sys.argv[1] +prefix, line_prefix, separator, line_suffix, suffix = langs[lang] + +f = urllib2.urlopen('http://data.iana.org/TLD/tlds-alpha-by-domain.txt') +tlds = [] +output_line = "" +for input_line in f: + if input_line.startswith('#'): + continue + + tld = input_line.strip().lower() + new_output_line = output_line + prefix + tld + if len(new_output_line) > 60: + print output_line + line_suffix + output_line = line_prefix + tld + else: + output_line = new_output_line + prefix = separator + +print output_line + suffix diff --git a/admin/library-name b/admin/library-name new file mode 100644 index 0000000..c67163c --- /dev/null +++ b/admin/library-name @@ -0,0 +1 @@ +php-openid
\ No newline at end of file diff --git a/admin/longlines.pl b/admin/longlines.pl new file mode 100644 index 0000000..6ce65e8 --- /dev/null +++ b/admin/longlines.pl @@ -0,0 +1,46 @@ +#!/usr/bin/env perl -w + +use strict; + +my $filename = $ARGV[0]; + +if (!$filename) { + print "Usage: longlines.pl <filename> [length]\n"; + exit(1); +} + +# Set a default maximum line length. +my $max_length = $ARGV[1] || 80; + +my @lines = (); +my $line_num = 0; + +open(HANDLE, "<", $filename) or die "Cannot open $filename\n"; + +# Read the file and track the lines with length > $max_length. +while (<HANDLE>) { + $line_num++; + # Subtract one because the newline doesn't count toward the + # length. + if (length($_) - 1 > $max_length) { + push @lines, $line_num; + } +} + +# If more than five long lines were found, truncate to five and +# indicate that others were present, too. +if (@lines > 5) { + @lines = @lines[0..4]; + push @lines, "and others"; +} + +# If any long lines were found, notify and exit(1); otherwise, +# exit(0). +if (@lines) { + print $filename." (line".((@lines > 1) ? "s" : "")." ". + join(", ", @lines)." exceed".((@lines == 1) ? "s" : ""). + " length $max_length)\n"; + exit(1); +} else { + exit(0); +} diff --git a/admin/makedoc.sh b/admin/makedoc.sh new file mode 100644 index 0000000..f07f6c6 --- /dev/null +++ b/admin/makedoc.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -v +phpdoc -p on -t doc -d Auth,admin/tutorials -ti "JanRain OpenID Library" \ + --ignore \*~,BigMath.php,Discover.php,CryptUtil.php,DiffieHellman.php,HMACSHA1.php,KVForm.php,Parse.php,TrustRoot.php,HTTPFetcher.php,ParanoidHTTPFetcher.php,PlainHTTPFetcher.php,ParseHTML.php,URINorm.php,XRI.php,XRIRes.php,Misc.php \ + -dn "OpenID" -o "HTML:frames:phphtmllib" diff --git a/admin/mathlib b/admin/mathlib new file mode 100644 index 0000000..53f50d0 --- /dev/null +++ b/admin/mathlib @@ -0,0 +1,18 @@ +#!/usr/bin/env php +<?php + +require_once 'adminutil.php'; +includeAdd(getParent()); + +require_once 'Auth/OpenID/CryptUtil.php'; + +$lib =& Auth_OpenID_MathLibrary::getLibWrapper(); + +if ($lib === null) { + fwrite(STDERR, 'No math library present\n'); + exit(1); +} else { + print $lib->type; +} + +?> diff --git a/admin/nobadbraces b/admin/nobadbraces new file mode 100644 index 0000000..ee060dc --- /dev/null +++ b/admin/nobadbraces @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -e + +./admin/findphp | xargs -L 1 /usr/bin/env perl admin/brace_style.pl diff --git a/admin/nobadcase b/admin/nobadcase new file mode 100644 index 0000000..c2c2662 --- /dev/null +++ b/admin/nobadcase @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +bad=$(./admin/findphp | xargs egrep -n "^[^\'\"]*\b(TRUE|FALSE|NULL)\b") + +if [ ! -z "$bad" ] + then + cat <<EOF 1>&2 +These files contain wrongly capitalized constants: + +$bad + +EOF + exit 1 +fi diff --git a/admin/nolonglines b/admin/nolonglines new file mode 100644 index 0000000..3e2addc --- /dev/null +++ b/admin/nolonglines @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +files_with_long_lines=$(./admin/findphp | + xargs -L 1 -I FILENAME /usr/bin/env perl admin/longlines.pl FILENAME 80) + +if [ "$files_with_long_lines" ] + then + cat <<EOF 1>&2 +Found lines > 80 characters in: + +$files_with_long_lines +EOF + exit 1 +fi diff --git a/admin/notabs b/admin/notabs new file mode 100644 index 0000000..b6a7df0 --- /dev/null +++ b/admin/notabs @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# +# Look in the local directory for PHP files that have tabs in them. If +# there are files with tabs, print the list of files and exit with +# non-zero status. + +tabs=$(./admin/findphp | xargs egrep -n ' ' | sort) + +if [ ! -z "$tabs" ] + then + cat <<EOF 1>&2 +Found tabs in: +$tabs +EOF + exit 1 +fi diff --git a/admin/open_tag b/admin/open_tag new file mode 100644 index 0000000..27fe1c5 --- /dev/null +++ b/admin/open_tag @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +bad_files=$(./admin/findphp | + xargs -L 1 grep -H -m 1 "<?php" -c | + grep ":0" | + awk -F: '{ print $1 }') + +if [ "$bad_files" ] + then + cat <<EOF 1>&2 +These PHP files do NOT begin with <?php : + +$bad_files + +EOF + exit 1 +fi diff --git a/admin/otb_test.php b/admin/otb_test.php new file mode 100644 index 0000000..b2fc99f --- /dev/null +++ b/admin/otb_test.php @@ -0,0 +1,20 @@ +<?php + +function fail1() { +} + +function pass1() +{ +} + +function fail2( + ) { + +} + +function pass2( + ) +{ +} + +?>
\ No newline at end of file diff --git a/admin/package.xml b/admin/package.xml new file mode 100644 index 0000000..c959ba9 --- /dev/null +++ b/admin/package.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="ISO-8859-1" ?> +<package version="1.0"> + <name>%(package_name)s</name> + <summary>%(package_summary)s</summary> + <description> + %(package_description)s + </description> + <license>%(license_name)s</license> + %(maintainers)s + <release> + <version>%(version)s</version> + <date>%(date)s</date> + <state>%(release_stability)s</state> + <notes> + <![CDATA[ + %(release_notes)s + ]]> + </notes> + %(contents_version_1)s + </release> + <deps> + <dep type="php" rel="ge" version="4.3.0" /> + </deps> +</package> diff --git a/admin/package2.xml b/admin/package2.xml new file mode 100644 index 0000000..d3bd525 --- /dev/null +++ b/admin/package2.xml @@ -0,0 +1,74 @@ +<?xml version="1.0"?> +<package version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" + xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 +http://pear.php.net/dtd/tasks-1.0.xsd +http://pear.php.net/dtd/package-2.0 +http://pear.php.net/dtd/package-2.0.xsd"> + <name>%(package_name)s</name> + <uri>%(uri)s</uri> + <summary>%(package_summary)s</summary> + <description> + %(package_description)s + </description> + %(leads)s + <date>%(date)s</date> + <version> + <release>%(version)s</release> + <api>%(version)s</api> + </version> + <stability> + <release>%(release_stability)s</release> + <api>%(release_stability)s</api> + </stability> + <license uri="%(license_uri)s">%(license_name)s</license> + <notes> + <![CDATA[ + %(release_notes)s + ]]> + </notes> + <contents> + %(contents)s + </contents> + <dependencies> + <required> + <php> + <min>4.3.0</min> + </php> + <pearinstaller> + <min>1.4.5</min> + </pearinstaller> + </required> + <optional> + <package> + <name>PHPUnit</name> + <channel>pear.php.net</channel> + <min>1.1.1</min> + </package> + <package> + <name>PEAR_DB</name> + <channel>pear.php.net</channel> + <min>1.80</min> + </package> + <extension> + <name>pgsql</name> + </extension> + <extension> + <name>mysql</name> + </extension> + <extension> + <name>sqlite</name> + </extension> + <extension> + <name>bcmath</name> + </extension> + <extension> + <name>gmp</name> + </extension> + </optional> + </dependencies> + <!-- There really isn't much we should put in the phprelease tag, + although we should probably make a windows-specific release tag. --> + <phprelease/> +</package> diff --git a/admin/packagexml.py b/admin/packagexml.py new file mode 100644 index 0000000..e832405 --- /dev/null +++ b/admin/packagexml.py @@ -0,0 +1,155 @@ +#!/usr/bin/python + +import os +import os.path + +def makeMaintainerXML(leads): + maintainer_template = """ + <maintainer> + <user>%(user)s</user> + <name>%(name)s</name> + <email>%(email)s</email> + <role>lead</role> + </maintainer> + """ + + return "<maintainers>" + \ + "".join([maintainer_template % l for l in leads]) + \ + "</maintainers>" + +def makeLeadXML(leads): + lead_template = """ +<lead> + <name>%(name)s</name> + <user>%(user)s</user> + <email>%(email)s</email> + <active>%(active)s</active> +</lead> + """ + + return "".join([lead_template % l for l in leads]) + +INDENT_STRING = " " + +def buildContentsXMLFordir(dir_or_file, roles, depth=0, dir_role=None, + all_files=False): + """ + Returns a list of strings, each of which is either a <file> XML + element for the given file or a <dir> element which contains other + <file> elements. + """ + + try: + entries = os.listdir(dir_or_file) + dir_role_s = '' + if dir_role: + dir_role_s = ' role="%s"' % (dir_role) + lines = ['%s<dir name="%s"%s>' % (INDENT_STRING * depth, + os.path.basename(dir_or_file), + dir_role_s)] + + for entry in entries: + lines += buildContentsXMLFordir(dir_or_file + os.sep + entry, roles, + depth + 1, dir_role, all_files) + + lines.append('%s</dir>' % (INDENT_STRING * depth)) + + return lines + except OSError: + try: + extension = dir_or_file.split(".")[-1] + except: + if not all_files: + return [] + + if all_files and dir_role: + return ['%s<file name="%s" role="%s" />' % + (INDENT_STRING * depth, os.path.basename(dir_or_file), dir_role)] + elif extension in roles: # Ends in an extension we care about + return ['%s<file name="%s" role="%s" />' % + (INDENT_STRING * depth, os.path.basename(dir_or_file), + roles[extension])] + else: + return [] + +def buildContentsXML(roles, *dirs): + lines = [] + + for directory in dirs: + lines.append("\n".join(buildContentsXMLFordir(directory, roles, 1))) + + return "\n".join(lines) + +def buildDocsXML(*dirs): + lines = [] + + for directory in dirs: + lines.append("\n".join(buildContentsXMLFordir(directory, {}, 1, 'doc', + all_files=True))) + + return "\n".join(lines) + +if __name__ == "__main__": + def usage(progname): + print "Usage: %s <package version> <xml template file> <release notes file>" % (progname) + + import sys + import time + + try: + import xmlconfig + except: + print "Could not import XML configuration module xmlconfig" + sys.exit(1) + + # Expect sys.argv[2] to be the name of the XML file template to + # use for processing. + try: + template_f = open(sys.argv[2], 'r') + except Exception, e: + usage(sys.argv[0]) + print "Could not open template file:", str(e) + sys.exit(1) + + # Expect sys.argv[1] to be the version number to include in the + # package.xml file. + try: + version = sys.argv[1] + except: + usage(sys.argv[0]) + sys.exit(2) + + # Expect sys.argv[3] to be the name of the release notes file. + try: + release_file = sys.argv[3] + release_file_h = open(release_file, 'r') + release_notes = release_file_h.read().strip() + release_file_h.close() + except Exception, e: + usage(sys.argv[0]) + print str(e) + sys.exit(3) + + data = xmlconfig.__dict__.copy() + + contentsXml = buildContentsXML({'php': 'php'}, *xmlconfig.contents_dirs) + docsXml = buildDocsXML(*xmlconfig.docs_dirs) + + contents = '<dir name="/">\n' + contentsXml + \ + "\n" + docsXml + '\n </dir>' + + contents_v1 = '<filelist><dir name="/" baseinstalldir="Auth">\n' + contentsXml + \ + "\n" + docsXml + '\n </dir></filelist>' + + data['contents'] = contents + data['contents_version_1'] = contents_v1 + data['leads'] = makeLeadXML(xmlconfig.leads) + data['maintainers'] = makeMaintainerXML(xmlconfig.leads) + data['date'] = time.strftime("%Y-%m-%d") + data['version'] = version + data['uri'] = "%s%s-%s.tgz" % (data['package_base_uri'], data['package_name'], + version) + data['release_notes'] = release_notes + + template_data = template_f.read() + print template_data % data diff --git a/admin/phpaliases.py b/admin/phpaliases.py new file mode 100644 index 0000000..c4ce216 --- /dev/null +++ b/admin/phpaliases.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python + +"""This script searches files for functions that are just aliases in +PHP source code. This is not 100% reliable, so it should not be +automated, but it's useful to run once in a while to make sure that +all of the matches it finds are not really legitimate aliases. + +Usage: + + parse_aliases.py <name of alias file> [PHP source code filename]... +""" + +import sys + +# Fetch this URL to get the file that is parsed into the aliases list +alias_url = 'http://www.zend.com/phpfunc/all_aliases.php' + +header_tok = '<!-- END OF HEADER -->'; +footer_tok = '<!-- FOOTER -->'; + +# Example line of the table that we parse: +# '<tr bgcolor="#EFEFFF"><td><a href="function.bzclose.php">bzclose</a></td><td><a href="http://lxr.php.net/source/php-src/ext/bz2/bz2.c#48">php-src/ext/bz2/bz2.c</a></td><td><a href="function.fclose.php">fclose</a></td></tr>' + +import re + +line_re = re.compile(r''' +\A + +<tr\ bgcolor="[^">]+"> + +<td><a\ href="[^>"]+\.php">([^<>]+)</a></td> + +<td><a\ href="[^">]+">[^<>]+</a></td> + +<td> +(?: + <a\ href="[^">]+\.php"> + ( [^<>]+ ) + </a> +| ( [^<>]+ ) +) +</td> + +</tr> + +\Z +''', re.VERBOSE) + +def parseString(s): + _, rest = s.split(header_tok, 1) + body, _ = rest.split(footer_tok, 1) + + lines = body.split('\n') + assert [s.strip() for s in lines[-2:]] == ['</table>', ''] + assert lines[0].strip().startswith('<table') + del lines[0], lines[-2:] + aliases = {} + for line in lines: + mo = line_re.match(line) + assert mo, line + alias, master1, master2 = mo.groups() + if master1: + master = master1 + else: + assert master2 + master = master2 + aliases[alias] = master + + return aliases + +def parseFile(f): + return parseString(f.read()) + +def parseFileName(fn): + return parseFile(file(fn, 'r')) + +def parseURL(url): + return parseFile(urllib2.urlopen(url)) + +def getAliasRE(aliases): + return re.compile(r'(->|\$|)\s*\b(%s)\b' % ('|'.join(aliases.keys()))) + +def checkAliasesFile(alias_re, f): + found = [] + line_num = 1 + for line in f: + for mo in alias_re.finditer(line): + if mo.group(1): + continue + alias = mo.group(2) + found.append((line_num, alias)) + line_num += 1 + return found + +def checkAliases(alias_re, filename): + return checkAliasesFile(alias_re, file(filename, 'r')) + +def checkAliasesFiles(alias_re, filenames): + found = [] + for filename in filenames: + file_found = checkAliases(alias_re, filename) + found.extend([(filename, n, a) for (n, a) in file_found]) + return found + +def dumpResults(aliases, found, out=sys.stdout): + for filename, n, a in found: + print >>out, "%s:%d %s -> %s" % (filename, n, a, aliases[a]) + +def main(alias_file, *filenames): + aliases = parseFileName(alias_file) + alias_re = getAliasRE(aliases) + found = checkAliasesFiles(alias_re, filenames) + dumpResults(aliases, found) + return found + +if __name__ == '__main__': + found = main(*sys.argv[1:]) + if found: + sys.exit(1) diff --git a/admin/prepare-release b/admin/prepare-release new file mode 100644 index 0000000..98415a9 --- /dev/null +++ b/admin/prepare-release @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# +# Prepare this repository for release + +REPO_ROOT=$(dirname $(dirname $(readlink --canonicalize "$0"))) +cd "$REPO_ROOT" + +bash ./admin/fixperms + +./admin/makedoc.sh + +darcs changes --from-tag=. --summary > CHANGELOG diff --git a/admin/runtests b/admin/runtests new file mode 100644 index 0000000..b018c87 --- /dev/null +++ b/admin/runtests @@ -0,0 +1,71 @@ +#!/usr/bin/env bash + +# Get the absolute path containing this script +cd $(dirname "$0") +HERE=$PWD + +test_import () { + ./admin/checkimports +} + +test_syntax () { + ./admin/syntaxcheck +} + +test_tabs () { + /usr/bin/env bash "$HERE/notabs" +} + +test_longlines () { + /usr/bin/env bash "$HERE/nolonglines" +} + +test_nobadbraces () { + /usr/bin/env bash "$HERE/nobadbraces" +} + +test_nobadcase () { + /usr/bin/env bash "$HERE/nobadcase" +} + +test_opentag () { + /usr/bin/env bash "$HERE/open_tag" +} + +test_docblocks () { + /usr/bin/env bash "$HERE/docblocks" +} + +test_php () { + if uname -a | grep -i cygwin >/dev/null 2>/dev/null ; then + /usr/bin/env php "$(dirname "$0")/texttest.php" --insecure-rand \ + $TEXTTEST_ARGS + else + /usr/bin/env php "$HERE/texttest.php" $TEXTTEST_ARGS + fi +} + +tests="syntax tabs longlines nobadbraces nobadcase opentag docblocks php import" + +failures= + +# Run in repository root (parent of this directory) +cd $(dirname "$HERE") + +chmod +x ./admin/fixperms +./admin/fixperms + +for test_name in $tests + do + echo "Running test $test_name" 1>&2 + if ! eval "test_$test_name" + then + failures="$failures $test_name" + fi +done + +if [ ! -z "$failures" ] + then + echo "Failures in: $failures" 1>&2 + exit 1 +fi diff --git a/admin/syntaxcheck b/admin/syntaxcheck new file mode 100644 index 0000000..f94b7fa --- /dev/null +++ b/admin/syntaxcheck @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +for file in `./admin/findallphp` + do php -l $file +done diff --git a/admin/texttest.php b/admin/texttest.php new file mode 100644 index 0000000..b1c0a3c --- /dev/null +++ b/admin/texttest.php @@ -0,0 +1,197 @@ +<?php + +require_once 'Tests/TestDriver.php'; +require_once 'PHPUnit/TestResult.php'; +require_once 'Console/Getopt.php'; + +class TextTestResult extends PHPUnit_TestResult { + function addError(&$test, &$t) + { + parent::addError($test, $t); + echo "E"; + } + + function addFailure(&$test, &$t) + { + parent::addFailure($test, $t); + echo "F"; + } + + function addPassedTest(&$test) + { + parent::addPassedTest($test); + echo "."; + } + + function dumpBadResults() + { + foreach ($this->failures() as $failure) { + echo $failure->toString(); + } + + foreach ($this->errors() as $failure) { + echo $failure->toString(); + } + } +} + +function microtime_float() +{ + list($usec, $sec) = explode(" ", microtime()); + return ((float)$usec + (float)$sec); +} + +$longopts = array('no-math', + 'buggy-gmp', + 'no-curl', + 'math-lib=', + 'insecure-rand', + 'thorough', + 'extra-tests='); + +$con = new Console_Getopt; +$args = $con->readPHPArgv(); +array_shift($args); +$options = $con->getopt2($args, "", $longopts); + +if (PEAR::isError($options)) { + print $options->message . "\n"; + exit(1); +} + +list($flags, $tests_to_run) = $options; + +$math_type = array(); +$extra_test_modules = array(); +$thorough = false; +foreach ($flags as $flag) { + list($option, $value) = $flag; + switch ($option) { + case '--insecure-rand': + define('Auth_OpenID_RAND_SOURCE', null); + break; + case '--no-math': + define('Auth_OpenID_NO_MATH_SUPPORT', true); + break; + case '--buggy-gmp': + define('Auth_OpenID_BUGGY_GMP', true); + break; + case '--no-curl': + define('Auth_Yadis_CURL_OVERRIDE', true); + break; + case '--thorough': + define('Tests_Auth_OpenID_thorough', true); + break; + case '--extra-tests': + $extra_test_modules[] = $value; + break; + default: + print "Unrecognized option: $option\n"; + exit(1); + } +} + +// ******** Math library selection *********** +// XXX FIXME +// case '--math-lib': +// $math_type[] = $value; +// break; +if ($math_type && false) { + if (defined('Auth_OpenID_NO_MATH_SUPPORT')) { + print "--no-math and --math-lib are mutually exclusive\n"; + exit(1); + } + require_once('Auth/OpenID/BigMath.php'); + $new_extensions = array(); + foreach ($math_type as $lib) { + $found = false; + foreach (Auth_OpenID_math_extensions() as $ext) { + if ($ext['extension'] == $lib) { + $new_extensions[] = $ext; + $found = true; + break; + } + } + + if (!$found) { + print "Unknown math library specified: $lib\n"; + exit(1); + } + } + $_Auth_OpenID_math_extensions = $new_extensions; +} + +// ******** End math library selection ********** + +$suites = loadSuite($tests_to_run); + +// ******** Load additional test suites ******** +foreach ($extra_test_modules as $filename) { + if (!global_require_once($filename)) { + continue; + } + $module_name = basename($filename, '.php'); + $class_name = "Tests_Auth_OpenID_${module_name}_Test"; + $suites[] = makeSuite($class_name); +} + + +$totals = array( + 'run' => 0, + 'error' => 0, + 'failure' => 0, + 'time' => 0 + ); + +foreach ($suites as $suite) { + $name = $suite->getName(); + echo "========================================== +Test suite: $name +------------------------------------------ +"; + + $result = new TextTestResult(); + $before = microtime_float(); + $suite->run($result); + $after = microtime_float(); + + $run = $result->runCount(); + $error = $result->errorCount(); + $failure = $result->failureCount(); + $delta = $after - $before; + $totals['run'] += $run; + $totals['error'] += $error; + $totals['failure'] += $failure; + $totals['time'] += $delta; + $human_delta = round($delta, 3); + echo "\nRan $run tests in $human_delta seconds"; + if ($error || $failure) { + echo " with $error errors, $failure failures"; + } + echo " +========================================== + +"; + + $failures = $result->failures(); + foreach($failures as $failure) { + $test = $failure->failedTest(); + $testName = $test->getName(); + $exception = $failure->thrownException(); + echo "* Failure in $testName: $exception + +"; + } +} + +$before = microtime_float(); +$run = $totals['run']; +$error = $totals['error']; +$failure = $totals['failure']; +$time = round($totals['time'], 3); +echo "Ran a total of $run tests in $time seconds with $error errors, $failure failures\n"; +if ($totals['error'] || $totals['failure']) { + exit(1); +} + +?> diff --git a/admin/tutorials/OpenID/OpenID.pkg b/admin/tutorials/OpenID/OpenID.pkg new file mode 100644 index 0000000..3f6c996 --- /dev/null +++ b/admin/tutorials/OpenID/OpenID.pkg @@ -0,0 +1,82 @@ +<refentry id="{@id}"> + <refnamediv> + <refname>PHP OpenID API</refname> + + </refnamediv> + <refsynopsisdiv> + <author> + JanRain, Inc. + <authorblurb> + {@link mailto:openid@janrain.com openid@janrain.com} + </authorblurb> + </author> + </refsynopsisdiv> + + <para> + This is a complete implementation of the OpenID authentication + protocol. This package contains: + + <itemizedlist> + <listitem>An OpenID server</listitem> + <listitem>An OpenID consumer</listitem> + <listitem>Stores for MySQL, PostgreSQL, SQLite, and filesystem-based OpenID data storage</listitem> + <listitem>PHPUnit unit tests</listitem> + <listitem>An example server and consumer</listitem> + </itemizedlist> + </para> + + <refsect1 id="{@id users}"> + <title>For Package Users</title> + + <para> + To install this package, copy the <literal>Auth/</literal> + directory in this package to a directory in your PHP include path. + Alternatively, modify your PHP include path to include the + directory that contains <literal>Auth/</literal>. Any + applications that need this package will then be able to use it. + </para> + + </refsect1> + + <refsect1 id="{@id developers}"> + <title>For Developers</title> + + <para> + + See the server and consumer examples in the + <literal>examples/</literal> directory of the package. For + details on how to run the examples, see + <literal>examples/README</literal>. If you want to create your + own OpenID data storage class, please see the {@link Auth_OpenID_OpenIDStore} + class. For information on integrating OpenID relying party support into your site, see + the class documentation for {@link Consumer.php}. + + </para> + + </refsect1> + + <refsect1 id="{@id references}"> + <title>References</title> + <para> + <itemizedlist> + <listitem> + {@link http://www.openidenabled.com/openid/libraries/php PHP OpenID Library} + </listitem> + + <listitem> + {@link http://www.janrain.com JanRain, Inc.} + </listitem> + + <listitem> + {@link http://lists.openidenabled.com/mailman/listinfo/dev OpenID Development Discussion List} + </listitem> + + <listitem> + {@link http://www.openidenabled.com/yadis/libraries/php PHP Yadis Library} + </listitem> + </itemizedlist> + </para> + </refsect1> + +</refentry> + diff --git a/admin/webtest.php b/admin/webtest.php new file mode 100644 index 0000000..1076623 --- /dev/null +++ b/admin/webtest.php @@ -0,0 +1,12 @@ +<?php + +require_once 'Tests/TestDriver.php'; + +$suites = loadSuite(); + +// Create and run the user interface +$gui = new PHPUnit_GUI_HTML(); +$gui->addSuites($suites); +$gui->show(); + +?> diff --git a/admin/xmlconfig.py b/admin/xmlconfig.py new file mode 100644 index 0000000..ac029b5 --- /dev/null +++ b/admin/xmlconfig.py @@ -0,0 +1,55 @@ + +""" +This is the package.xml data needed for the PHP OpenID PEAR +package.xml file. Use the 'packagexml.py' program to generate a +package.xml file for a release of this library. +""" + +# This is a list of dicts describing the project leads. This will be +# used to generate <lead> XML elements. +leads = [ + {'name': 'Jonathan Daugherty', + 'user': 'cygnus', + 'email': 'cygnus@janrain.com', + 'active': 'yes'}, + {'name': 'Josh Hoyt', + 'user': 'jhoyt', + 'email': 'josh@janrain.com', + 'active': 'yes'} + ] + +# The package name. +package_name = 'Auth_OpenID' + +# The package description. +package_description = 'An implementation of the OpenID single sign-on authentication protocol.' + +# Package summary. +package_summary = 'PHP OpenID' + +# License string. +license_name = 'Apache' + +# License URI. +license_uri = 'http://www.apache.org/licenses/LICENSE-2.0' + +# Director(ies) containing package source, relative to the admin/ +# directory. All .php files in these directories will be included in +# the <contents> element of the output XML and will be assigned the +# role 'php'. +contents_dirs = ['../Auth',] + +# Director(ies) containing package documentation. All files and +# subdirectories in these directories will be included in the +# <contents> element in the output XML and will be assigned the role +# 'doc'. +docs_dirs = ['../doc', '../examples'] + +# The HTTP package base URI. This is the place on the web where the +# PEAR-installable tarballs will live, and this (plus the package +# tarball name) will be the URL that users pass to "pear install". +package_base_uri = 'http://www.openidenabled.com/resources/downloads/php-openid/pear/' + +# The release stability. Maybe this should be a commandline parameter +# since it might differ from release to release. +release_stability = 'stable' diff --git a/contrib/signed_assertions/AP.php b/contrib/signed_assertions/AP.php new file mode 100644 index 0000000..a242650 --- /dev/null +++ b/contrib/signed_assertions/AP.php @@ -0,0 +1,180 @@ +<?php + +/** + * Introduces the notion of an Attribute Provider that attests and signs + * attributes + * Uses OpenID Signed Assertions(Sxip draft) for attesting attributes + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author Santosh Subramanian <subrasan@cs.sunysb.edu> + * @author Shishir Randive <srandive@cs.sunysb.edu> + * Stony Brook University. + * + */ +require_once 'Auth/OpenID/SAML.php'; +/** + * The Attribute_Provider class which signs the attribute,value pair + * for a given openid. + */ +class Attribute_Provider +{ + private $public_key_certificate=null; + private $private_key=null; + private $authenticatedUser=null; + private $notBefore=null; + private $notOnOrAfter=null; + private $rsadsa=null; + private $acsURI=null; + private $attribute=null; + private $value=null; + private $assertionTemplate=null; + /** + * Creates an Attribute_Provider object initialized with startup values. + * @param string $public_key_certificate - The public key certificate + of the signer. + * @param string $private_key - The private key of the signer. + * @param string $notBefore - Certificate validity time + * @param string $notOnOrAfter - Certificate validity time + * @param string $rsadsa - Choice of the algorithm (RSA/DSA) + * @param string $acsURI - URI of the signer. + * @param string $assertionTemplate - SAML template used for assertion + */ + function Attribute_Provider($public_key_certificate,$private_key,$notBefore,$notOnOrAfter,$rsadsa,$acsURI, + $assertionTemplate) + { + $this->public_key_certificate=$public_key_certificate; + $this->private_key=$private_key; + $this->notBefore=$notBefore; + $this->notOnOrAfter=$notOnOrAfter; + $this->rsadsa=$rsadsa; + $this->acsURI=$acsURI; + $this->assertionTemplate=$assertionTemplate; + } + /** + * Create the signed assertion. + * @param string $openid - Openid of the entity being asserted. + * @param string $attribute - The attribute name being asserted. + * @param string $value - The attribute value being asserted. + */ + function sign($openid,$attribute,$value) + { + $samlObj = new SAML(); + $responseXmlString = $samlObj->createSamlAssertion($openid, + $this->notBefore, + $this->notOnOrAfter, + $this->rsadsa, + $this->acsURI, + $attribute, + sha1($value), + $this->assertionTemplate); + $signedAssertion=$samlObj->signAssertion($responseXmlString, + $this->private_key, + $this->public_key_certificate); + return $signedAssertion; + } +} +/** + * The Attribute_Verifier class which verifies the signed assertion at the Relying party. + */ +class Attribute_Verifier +{ + /** + * The certificate the Relying party trusts. + */ + private $rootcert; + /** + * This function loads the public key certificate that the relying party trusts. + * @param string $cert - Trusted public key certificate. + */ + function load_trusted_root_cert($cert) + { + $this->rootcert=$cert; + } + /** + * Verifies the certificate given the SAML document. + * @param string - signed SAML assertion + * return @boolean - true if verification is successful, false if unsuccessful. + */ + function verify($responseXmlString) + { + $samlObj = new SAML(); + $ret = $samlObj->verifyAssertion($responseXmlString,$this->rootcert); + return $ret; + } +} + +/** + * This is a Store Request creating class at the Attribute Provider. + */ +class AP_OP_StoreRequest +{ + /** + * Creates store request and adds it as an extension to AuthRequest object + passed to it. + * @param &Auth_OpenID_AuthRequest &$auth_request - A reference to + the AuthRequest object. + * @param &Attribute_Provider &$attributeProvider - A reference to the + Attribute Provider object. + * @param string $attribute - The attribute name being asserted. + * @param string $value - The attribute value being asserted. + * @param string $openid - Openid of the entity being asserted. + * @return &Auth_OpenID_AuthRequest - Auth_OpenID_AuthRequest object + returned with StoreRequest extension. + */ + static function createStoreRequest(&$auth_request,&$attributeProvider, + $attribute,$value,$openid) + { + if(!$auth_request){ + return null; + } + $signedAssertion=$attributeProvider->sign($openid,$attribute,$value); + $store_request=new Auth_OpenID_AX_StoreRequest; + $store_request->addValue($attribute,base64_encode($value)); + $store_request->addValue($attribute.'/signature', + base64_encode($signedAssertion)); + if($store_request) { + $auth_request->addExtension($store_request); + return $auth_request; + } + } +} + +/* + *This is implemented at the RP Takes care of getting the attribute from the + *AX_Fetch_Response object and verifying it. + */ +class RP_OP_Verify +{ + /** + * Verifies a given signed assertion. + * @param &Attribute_Verifier &$attributeVerifier - An instance of the class + passed for the verification. + * @param Auth_OpenID_Response - Response object for extraction. + * @return boolean - true if successful, false if verification fails. + */ + function verifyAssertion(&$attributeVerifier,$response) + { + $ax_resp=Auth_OpenID_AX_FetchResponse::fromSuccessResponse($response); + if($ax_resp instanceof Auth_OpenID_AX_FetchResponse){ + $ax_args=$ax_resp->getExtensionArgs(); + if($ax_args) { + $value=base64_decode($ax_args['value.ext1.1']); + if($attributeVerifier->verify($value)){ + return base64_decode($ax_args['value.ext0.1']); + } else { + return null; + } + } else { + return null; + } + } else { + return null; + } + } +} + + +?> diff --git a/contrib/signed_assertions/SAML.php b/contrib/signed_assertions/SAML.php new file mode 100644 index 0000000..fa6df51 --- /dev/null +++ b/contrib/signed_assertions/SAML.php @@ -0,0 +1,220 @@ +<?php +/** + ** PHP versions 4 and 5 + ** + ** LICENSE: See the COPYING file included in this distribution. + ** + ** @package OpenID + ** @author Santosh Subramanian <subrasan@cs.sunysb.edu> + ** @author Shishir Randive <srandive@cs.sunysb.edu> + ** Stony Brook University. + ** largely derived from + ** + * Copyright (C) 2007 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +class SAML{ + private $assertionTemplate=null; + /** + * Returns a SAML response with various elements filled in. + * @param string $authenticatedUser The OpenId of the user + * @param string $notBefore The ISO 8601 formatted date before which the + response is invalid + * @param string $notOnOrAfter The ISO 8601 formatted data after which the + response is invalid + * @param string $rsadsa 'rsa' if the response will be signed with RSA keys, + 'dsa' for DSA keys + * @param string $requestID The ID of the request we're responding to + * @param string $destination The ACS URL that the response is submitted to + * @return string XML SAML response. + */ + function createSamlAssertion($authenticatedUser, $notBefore, $notOnOrAfter, $rsadsa, $acsURI,$attribute,$value,$assertionTemplate) + { + $samlResponse = $assertionTemplate; + $samlResponse = str_replace('USERNAME_STRING', $authenticatedUser, $samlResponse); + $samlResponse = str_replace('RESPONSE_ID', $this->samlCreateId(), $samlResponse); + $samlResponse = str_replace('ISSUE_INSTANT', $this->samlGetDateTime(time()), $samlResponse); + $samlResponse = str_replace('NOT_BEFORE', $this->samlGetDateTime(strtotime($notBefore)), $samlResponse); + $samlResponse = str_replace('NOT_ON_OR_AFTER', $this->samlGetDateTime(strtotime($notOnOrAfter)),$samlResponse); + $samlResponse = str_replace('ASSERTION_ID',$this->samlCreateId(), $samlResponse); + $samlResponse = str_replace('RSADSA', strtolower($rsadsa), $samlResponse); + $samlResponse = str_replace('ISSUER_DOMAIN', $acsURI, $samlResponse); + $samlResponse = str_replace('ATTRIBUTE_NAME', $attribute, $samlResponse); + $samlResponse = str_replace('ATTRIBUTE_VALUE', $value, $samlResponse); + return $samlResponse; + } + + /** + * Signs a SAML response with the given private key, and embeds the public key. + * @param string $responseXmlString The unsigned Assertion which will be signed + * @param string $priKey Private key to sign the certificate + * @param string $cert Public key certificate of signee + * @return string Signed Assertion + */ + function signAssertion($responseXmlString,$privKey,$cert) + { + if (file_exists("/tmp/xml")) { + $tempFileDir="/tmp/xml/"; + + } else { + mkdir("/tmp/xml",0777); + $tempFileDir="/tmp/xml/"; + } + $tempName = 'saml-response-' . $this->samlCreateId() . '.xml'; + $tempFileName=$tempFileDir.$tempName; + while (file_exists($tempFileName)) + $tempFileName = 'saml-response-' . $this->samlCreateId() . '.xml'; + + if (!$handle = fopen($tempFileName, 'w')) { + return null; + } + if (fwrite($handle, $responseXmlString) === false) { + return null; + } + fclose($handle); + $cmd = 'xmlsec1 --sign --privkey-pem ' . $privKey . + ',' . $cert . ' --output ' . $tempFileName . + '.out ' . $tempFileName; + exec($cmd, $resp); + unlink($tempFileName); + + $xmlResult = @file_get_contents($tempFileName . '.out'); + if (!$xmlResult) { + return null; + } else { + unlink($tempFileName . '.out'); + return $xmlResult; + } + } + + + /** + * Verify a saml response with the given public key. + * @param string $responseXmlString Response to sign + * @param string $rootcert trusted public key certificate + * @return string Signed SAML response + */ + function verifyAssertion($responseXmlString,$rootcert) + { + date_default_timezone_set("UTC"); + if (file_exists("/tmp/xml")) { + $tempFileDir="/tmp/xml/"; + + } else { + mkdir("/tmp/xml",0777); + $tempFileDir="/tmp/xml/"; + } + + $tempName = 'saml-response-' . $this->samlCreateId() . '.xml'; + $tempFileName=$tempFileDir.$tempName; + while (file_exists($tempFileName)) + $tempFileName = 'saml-response-' . $this->samlCreateId() . '.xml'; + + if (!$handle = fopen($tempFileName, 'w')) { + return false; + } + + if (fwrite($handle, $responseXmlString) === false) { + return false; + } + + $p=xml_parser_create(); + $result=xml_parse_into_struct($p,$responseXmlString,$vals,$index); + xml_parser_free($p); + $cert_info=$index["X509CERTIFICATE"]; + $conditions=$index["CONDITIONS"]; + foreach($cert_info as $key=>$value){ + file_put_contents($tempFileName.'.cert',$vals[$value]['value']); + } + $cert=$tempFileName.'.cert'; + $before=0; + $after=0; + foreach($conditions as $key=>$value){ + $before=$vals[$value]['attributes']['NOTBEFORE']; + $after=$vals[$value]['attributes']['NOTONORAFTER']; + } + $before=$this->validSamlDateFormat($before); + $after=$this->validSamlDateFormat($after); + if(strtotime("now") < $before || strtotime("now") >= $after){ + unlink($tempFileName); + unlink($cert); + return false; + } + fclose($handle); + $cmd = 'xmlsec1 --verify --pubkey-cert ' . $cert .'--trusted '.$rootcert. ' '.$tempFileName.'* 2>&1 1>/dev/null'; + exec($cmd,$resp); + if(strcmp($resp[0],"FAIL") == 0){ + $value = false; + }elseif(strcmp($resp[0],"ERROR") == 0){ + $value = false; + }elseif(strcmp($resp[0],"OK") == 0){ + $value = TRUE; + } + unlink($tempFileName); + unlink($cert); + return $value; + } + + /** + * Creates a 40-character string containing 160-bits of pseudorandomness. + * @return string Containing pseudorandomness of 160 bits + */ + + function samlCreateId() + { + $rndChars = 'abcdefghijklmnop'; + $rndId = ''; + for ($i = 0; $i < 40; $i++ ) { + $rndId .= $rndChars[rand(0,strlen($rndChars)-1)]; + } + return $rndId; + } + + /** + * Returns a unix timestamp in xsd:dateTime format. + * @param timestamp int UNIX Timestamp to convert to xsd:dateTime + * ISO 8601 format. + * @return string + */ + function samlGetDateTime($timestamp) + { + return gmdate('Y-m-d\TH:i:s\Z', $timestamp); + } + /** + * Attempts to check whether a SAML date is valid. Returns true or false. + * @param string $samlDate + * @return bool + */ + + function validSamlDateFormat($samlDate) + { + if ($samlDate == "") return false; + $indexT = strpos($samlDate, 'T'); + $indexZ = strpos($samlDate, 'Z'); + if (($indexT != 10) || ($indexZ != 19)) { + return false; + } + $dateString = substr($samlDate, 0, 10); + $timeString = substr($samlDate, $indexT + 1, 8); + list($year, $month, $day) = explode('-', $dateString); + list($hour, $minute, $second) = explode(':', $timeString); + $parsedDate = gmmktime($hour, $minute, $second, $month, $day, $year); + if (($parsedDate === false) || ($parsedDate == -1)) return false; + if (!checkdate($month, $day, $year)) return false; + return $parsedDate; + } + +} +?> diff --git a/contrib/upgrade-store-1.1-to-2.0 b/contrib/upgrade-store-1.1-to-2.0 new file mode 100644 index 0000000..1f587c3 --- /dev/null +++ b/contrib/upgrade-store-1.1-to-2.0 @@ -0,0 +1,170 @@ +#!/usr/bin/env python +# SQL Store Upgrade Script +# for version 1.x to 2.0 of the OpenID library. +# Doesn't depend on the openid library, so you can run this python +# script to update databases for ruby or PHP as well. +# +# Testers note: +# +# A SQLite3 db with the 1.2 schema exists in +# openid/test/data/openid-1.2-consumer-sqlitestore.db if you want something +# to try upgrading. +# +# TODO: +# * test data for mysql and postgresql. +# * automated tests. + +import os +import getpass +import sys +from optparse import OptionParser + + +def askForPassword(): + return getpass.getpass("DB Password: ") + +def askForConfirmation(dbname,tablename): + print """The table %s from the database %s will be dropped, and + an empty table with the new nonce table schema will replace it."""%( + tablename, dbname) + return raw_input("Continue? ").lower().strip().startswith('y') + +def doSQLiteUpgrade(db_conn, nonce_table_name='oid_nonces'): + cur = db_conn.cursor() + cur.execute('DROP TABLE %s'%nonce_table_name) + sql = """ + CREATE TABLE %s ( + server_url VARCHAR, + timestamp INTEGER, + salt CHAR(40), + UNIQUE(server_url, timestamp, salt) + ); + """%nonce_table_name + cur.execute(sql) + cur.close() + +def doMySQLUpgrade(db_conn, nonce_table_name='oid_nonces'): + cur = db_conn.cursor() + cur.execute('DROP TABLE %s'%nonce_table_name) + sql = """ + CREATE TABLE %s ( + server_url BLOB, + timestamp INTEGER, + salt CHAR(40), + PRIMARY KEY (server_url(255), timestamp, salt) + ) + TYPE=InnoDB; + """%nonce_table_name + cur.execute(sql) + cur.close() + +def doPostgreSQLUpgrade(db_conn, nonce_table_name='oid_nonces'): + cur = db_conn.cursor() + cur.execute('DROP TABLE %s'%nonce_table_name) + sql = """ + CREATE TABLE %s ( + server_url VARCHAR(2047), + timestamp INTEGER, + salt CHAR(40), + PRIMARY KEY (server_url, timestamp, salt) + ); + """%nonce_table_name + cur.execute(sql) + cur.close() + db_conn.commit() + +def main(argv=None): + parser = OptionParser() + parser.add_option("-u", "--user", dest="username", + default=os.environ.get('USER'), + help="User name to use to connect to the DB. " + "Defaults to USER environment variable.") + parser.add_option('-t', '--table', dest='tablename', default='oid_nonces', + help='The name of the nonce table to drop and recreate. ' + ' Defaults to "oid_nonces", the default table name for ' + 'the openid stores.') + parser.add_option('--mysql', dest='mysql_db_name', + help='Upgrade a table from this MySQL database. ' + 'Requires username for database.') + parser.add_option('--pg', '--postgresql', dest='postgres_db_name', + help='Upgrade a table from this PostgreSQL database. ' + 'Requires username for database.') + parser.add_option('--sqlite', dest='sqlite_db_name', + help='Upgrade a table from this SQLite database file.') + parser.add_option('--host', dest='db_host', + default='localhost', + help='Host on which to find MySQL or PostgreSQL DB.') + (options, args) = parser.parse_args(argv) + + db_conn = None + + if options.sqlite_db_name: + try: + from pysqlite2 import dbapi2 as sqlite + except ImportError: + print "You must have pysqlite2 installed in your PYTHONPATH." + return 1 + try: + db_conn = sqlite.connect(options.sqlite_db_name) + except Exception, e: + print "Could not connect to SQLite database:", str(e) + return 1 + + if askForConfirmation(options.sqlite_db_name, options.tablename): + doSQLiteUpgrade(db_conn, nonce_table_name=options.tablename) + + if options.postgres_db_name: + if not options.username: + print "A username is required to open a PostgreSQL Database." + return 1 + password = askForPassword() + try: + import psycopg + except ImportError: + print "You need psycopg installed to update a postgres DB." + return 1 + + try: + db_conn = psycopg.connect(database = options.postgres_db_name, + user = options.username, + host = options.db_host, + password = password) + except Exception, e: + print "Could not connect to PostgreSQL database:", str(e) + return 1 + + if askForConfirmation(options.postgres_db_name, options.tablename): + doPostgreSQLUpgrade(db_conn, nonce_table_name=options.tablename) + + if options.mysql_db_name: + if not options.username: + print "A username is required to open a MySQL Database." + return 1 + password = askForPassword() + try: + import MySQLdb + except ImportError: + print "You must have MySQLdb installed to update a MySQL DB." + return 1 + + try: + db_conn = MySQLdb.connect(options.db_host, options.username, + password, options.mysql_db_name) + except Exception, e: + print "Could not connect to MySQL database:", str(e) + return 1 + + if askForConfirmation(options.mysql_db_name, options.tablename): + doMySQLUpgrade(db_conn, nonce_table_name=options.tablename) + + if db_conn: + db_conn.close() + else: + parser.print_help() + + return 0 + + +if __name__ == '__main__': + retval = main() + sys.exit(retval) diff --git a/examples/README b/examples/README new file mode 100644 index 0000000..fd01ccb --- /dev/null +++ b/examples/README @@ -0,0 +1,134 @@ +OpenID Example Code +------------------- + +After you've installed this package (see ../README), you can use these +example packages to get started. They'll show you what this package +can do, and you can use them as the basis for your own OpenID support. + +consumer/: OpenID Example Consumer +================================== + +NOTE: If you want to try the example consumer without installing this +package, just make sure you add the package's 'Auth' directory to your +PHP include path. + +To try the example consumer implementation, just copy the consumer/ +directory into a place on your web server and point your browser at +the new directory. + +1. Check to be sure that /tmp is in your "open_basedir" configuration, + if open_basedir is being used to restrict PHP's file I/O. See + http://us2.php.net/features.safe-mode for more information. For + example, in your php.ini, change + + open_basedir = "..." + + to + + open_basedir = "/tmp:..." + + (If you really don't want to add /tmp to your open_basedir, you can + modify consumer/common.php and change $store_path so it doesn't + create the store directory in /tmp.) + +2. Copy or symlink the consumer/ directory into a part of your + webserver's docroot. For example, if your DocumentRoot is + /var/www/, do this: + + # cd /var/www + # ln -s /path/to/PHP-OpenID-X.Y.Z/examples/consumer + +3. Navigate to http://www.example.com/consumer and enter an OpenID + into the form presented there and click "Verify". + +consumer/ Files +=============== + +The 'index.php' file will render a form and get you started. These +are the example consumer files: + + consumer/index.php - Renders a form so you can begin the OpenID auth +process. The form submits the OpenID to try_auth.php. + + consumer/try_auth.php - Starts the authentication with the OpenID +server that manages your OpenID and redirects your browser to the +server's login page. Instructs the server to return to +finish_auth.php when you're done authenticating. + + consumer/finish_auth.php - Finishes the authentication by checking +the server's response. Tells you if the authentication was +successful. + + consumer/common.php - Includes the setup code you'll need to create +a Consumer object and participate in an OpenID authentication. + +server/: OpenID Example Server +============================== + +To try the example server, follow these steps: + +1. Copy or symlink the server/ directory into a part of your + webserver's docroot. For example, if your DocumentRoot is + /var/www/, do this: + + # cd /var/www + # ln -s /path/to/PHP-OpenID-X.Y.Z/examples/server + +2. Navigate to the server example. You'll be redirected to + server/setup.php where you can choose some configuration options to + generate a configuration. Once finished, you can download a file + "config.php." Save that file in the example server directory. + +The example server has the following features: + + - It serves its own identity pages, whose URLs are of the form + + http://.../server/server.php/idpage?user=USERNAME + + - It does not require passwords. + + - It does not support a "trusted sites" page, as you pointed out. + +In general, the example server is NOT supposed to be treated as a +fully-equiped OpenID server (i.e., with user accounts and other +state). It is supposed to demonstrate how to write PHP applications +that use the library. + +Upgrading from the 1.X.X example server +======================================= + +The 2.X.X library's example server is different from the 1.X.X example +server in the following ways: + + - The new example server does not support authenticating arbitrary + URLs. It serves its own URLs. This makes it easier to set up and + test. + + - The new example server does not support password authentication. + This makes it easier to set up and is not necessary for + illustrating the use of the library. + + - The new example server does not have a "trusted sites" page. + +server/ Files +============= + +These files make up the server example code: + + config.php - The configuration file you'll need to customize to run +the example server. + + server.php - The PHP rendering script that takes care of handling +server requests from both regular user agents and consumers. + + lib/actions.php - Handles the various types of requests that the +server supports. + + lib/common.php - Supplies functions that wrap the OpenID API calls +to make them easier to use. + + lib/render.php - Miscellaneous page rendering code. + + lib/session.php - Code to handle session data for user settings. + + lib/render/*.php - Files for each page presented by the server. diff --git a/examples/consumer/common.php b/examples/consumer/common.php new file mode 100644 index 0000000..2f01ba0 --- /dev/null +++ b/examples/consumer/common.php @@ -0,0 +1,97 @@ +<?php + +$path_extra = dirname(dirname(dirname(__FILE__))); +$path = ini_get('include_path'); +$path = $path_extra . PATH_SEPARATOR . $path; +ini_set('include_path', $path); + +function displayError($message) { + $error = $message; + include 'index.php'; + exit(0); +} + +function doIncludes() { + /** + * Require the OpenID consumer code. + */ + require_once "Auth/OpenID/Consumer.php"; + + /** + * Require the "file store" module, which we'll need to store + * OpenID information. + */ + require_once "Auth/OpenID/FileStore.php"; + + /** + * Require the Simple Registration extension API. + */ + require_once "Auth/OpenID/SReg.php"; + + /** + * Require the PAPE extension module. + */ + require_once "Auth/OpenID/PAPE.php"; +} + +doIncludes(); + +global $pape_policy_uris; +$pape_policy_uris = array( + PAPE_AUTH_MULTI_FACTOR_PHYSICAL, + PAPE_AUTH_MULTI_FACTOR, + PAPE_AUTH_PHISHING_RESISTANT + ); + +function &getStore() { + /** + * This is where the example will store its OpenID information. + * You should change this path if you want the example store to be + * created elsewhere. After you're done playing with the example + * script, you'll have to remove this directory manually. + */ + $store_path = "/tmp/_php_consumer_test"; + + if (!file_exists($store_path) && + !mkdir($store_path)) { + print "Could not create the FileStore directory '$store_path'. ". + " Please check the effective permissions."; + exit(0); + } + + return new Auth_OpenID_FileStore($store_path); +} + +function &getConsumer() { + /** + * Create a consumer object using the store object created + * earlier. + */ + $store = getStore(); + $consumer =& new Auth_OpenID_Consumer($store); + return $consumer; +} + +function getScheme() { + $scheme = 'http'; + if (isset($_SERVER['HTTPS']) and $_SERVER['HTTPS'] == 'on') { + $scheme .= 's'; + } + return $scheme; +} + +function getReturnTo() { + return sprintf("%s://%s:%s%s/finish_auth.php", + getScheme(), $_SERVER['SERVER_NAME'], + $_SERVER['SERVER_PORT'], + dirname($_SERVER['PHP_SELF'])); +} + +function getTrustRoot() { + return sprintf("%s://%s:%s%s/", + getScheme(), $_SERVER['SERVER_NAME'], + $_SERVER['SERVER_PORT'], + dirname($_SERVER['PHP_SELF'])); +} + +?>
\ No newline at end of file diff --git a/examples/consumer/finish_auth.php b/examples/consumer/finish_auth.php new file mode 100644 index 0000000..b19a665 --- /dev/null +++ b/examples/consumer/finish_auth.php @@ -0,0 +1,98 @@ +<?php + +require_once "common.php"; +session_start(); + +function escape($thing) { + return htmlentities($thing); +} + +function run() { + $consumer = getConsumer(); + + // Complete the authentication process using the server's + // response. + $return_to = getReturnTo(); + $response = $consumer->complete($return_to); + + // Check the response status. + if ($response->status == Auth_OpenID_CANCEL) { + // This means the authentication was cancelled. + $msg = 'Verification cancelled.'; + } else if ($response->status == Auth_OpenID_FAILURE) { + // Authentication failed; display the error message. + $msg = "OpenID authentication failed: " . $response->message; + } else if ($response->status == Auth_OpenID_SUCCESS) { + // This means the authentication succeeded; extract the + // identity URL and Simple Registration data (if it was + // returned). + $openid = $response->getDisplayIdentifier(); + $esc_identity = escape($openid); + + $success = sprintf('You have successfully verified ' . + '<a href="%s">%s</a> as your identity.', + $esc_identity, $esc_identity); + + if ($response->endpoint->canonicalID) { + $escaped_canonicalID = escape($response->endpoint->canonicalID); + $success .= ' (XRI CanonicalID: '.$escaped_canonicalID.') '; + } + + $sreg_resp = Auth_OpenID_SRegResponse::fromSuccessResponse($response); + + $sreg = $sreg_resp->contents(); + + if (@$sreg['email']) { + $success .= " You also returned '".escape($sreg['email']). + "' as your email."; + } + + if (@$sreg['nickname']) { + $success .= " Your nickname is '".escape($sreg['nickname']). + "'."; + } + + if (@$sreg['fullname']) { + $success .= " Your fullname is '".escape($sreg['fullname']). + "'."; + } + + $pape_resp = Auth_OpenID_PAPE_Response::fromSuccessResponse($response); + + if ($pape_resp) { + if ($pape_resp->auth_policies) { + $success .= "<p>The following PAPE policies affected the authentication:</p><ul>"; + + foreach ($pape_resp->auth_policies as $uri) { + $escaped_uri = escape($uri); + $success .= "<li><tt>$escaped_uri</tt></li>"; + } + + $success .= "</ul>"; + } else { + $success .= "<p>No PAPE policies affected the authentication.</p>"; + } + + if ($pape_resp->auth_age) { + $age = escape($pape_resp->auth_age); + $success .= "<p>The authentication age returned by the " . + "server is: <tt>".$age."</tt></p>"; + } + + if ($pape_resp->nist_auth_level) { + $auth_level = escape($pape_resp->nist_auth_level); + $success .= "<p>The NIST auth level returned by the " . + "server is: <tt>".$auth_level."</tt></p>"; + } + + } else { + $success .= "<p>No PAPE response was sent by the provider.</p>"; + } + } + + include 'index.php'; +} + +run(); + +?>
\ No newline at end of file diff --git a/examples/consumer/index.php b/examples/consumer/index.php new file mode 100644 index 0000000..343a39c --- /dev/null +++ b/examples/consumer/index.php @@ -0,0 +1,73 @@ +<?php +require_once "common.php"; + +global $pape_policy_uris; +?> +<html> + <head><title>PHP OpenID Authentication Example</title></head> + <style type="text/css"> + * { + font-family: verdana,sans-serif; + } + body { + width: 50em; + margin: 1em; + } + div { + padding: .5em; + } + table { + margin: none; + padding: none; + } + .alert { + border: 1px solid #e7dc2b; + background: #fff888; + } + .success { + border: 1px solid #669966; + background: #88ff88; + } + .error { + border: 1px solid #ff0000; + background: #ffaaaa; + } + #verify-form { + border: 1px solid #777777; + background: #dddddd; + margin-top: 1em; + padding-bottom: 0em; + } + </style> + <body> + <h1>PHP OpenID Authentication Example</h1> + <p> + This example consumer uses the <a + href="http://www.openidenabled.com/openid/libraries/php/">PHP + OpenID</a> library. It just verifies that the URL that you enter + is your identity URL. + </p> + + <?php if (isset($msg)) { print "<div class=\"alert\">$msg</div>"; } ?> + <?php if (isset($error)) { print "<div class=\"error\">$error</div>"; } ?> + <?php if (isset($success)) { print "<div class=\"success\">$success</div>"; } ?> + + <div id="verify-form"> + <form method="get" action="try_auth.php"> + Identity URL: + <input type="hidden" name="action" value="verify" /> + <input type="text" name="openid_identifier" value="" /> + + <p>Optionally, request these PAPE policies:</p> + <p> + <?php foreach ($pape_policy_uris as $i => $uri) { + print "<input type=\"checkbox\" name=\"policies[]\" value=\"$uri\" />"; + print "$uri<br/>"; + } ?> + </p> + + <input type="submit" value="Verify" /> + </form> + </div> + </body> +</html> diff --git a/examples/consumer/try_auth.php b/examples/consumer/try_auth.php new file mode 100644 index 0000000..7efec76 --- /dev/null +++ b/examples/consumer/try_auth.php @@ -0,0 +1,83 @@ +<?php + +require_once "common.php"; +session_start(); + +function getOpenIDURL() { + // Render a default page if we got a submission without an openid + // value. + if (empty($_GET['openid_identifier'])) { + $error = "Expected an OpenID URL."; + include 'index.php'; + exit(0); + } + + return $_GET['openid_identifier']; +} + +function run() { + $openid = getOpenIDURL(); + $consumer = getConsumer(); + + // Begin the OpenID authentication process. + $auth_request = $consumer->begin($openid); + + // No auth request means we can't begin OpenID. + if (!$auth_request) { + displayError("Authentication error; not a valid OpenID."); + } + + $sreg_request = Auth_OpenID_SRegRequest::build( + // Required + array('nickname'), + // Optional + array('fullname', 'email')); + + if ($sreg_request) { + $auth_request->addExtension($sreg_request); + } + + $policy_uris = $_GET['policies']; + + $pape_request = new Auth_OpenID_PAPE_Request($policy_uris); + if ($pape_request) { + $auth_request->addExtension($pape_request); + } + + // Redirect the user to the OpenID server for authentication. + // Store the token for this authentication so we can verify the + // response. + + // For OpenID 1, send a redirect. For OpenID 2, use a Javascript + // form to send a POST request to the server. + if ($auth_request->shouldSendRedirect()) { + $redirect_url = $auth_request->redirectURL(getTrustRoot(), + getReturnTo()); + + // If the redirect URL can't be built, display an error + // message. + if (Auth_OpenID::isFailure($redirect_url)) { + displayError("Could not redirect to server: " . $redirect_url->message); + } else { + // Send redirect. + header("Location: ".$redirect_url); + } + } else { + // Generate form markup and render it. + $form_id = 'openid_message'; + $form_html = $auth_request->htmlMarkup(getTrustRoot(), getReturnTo(), + false, array('id' => $form_id)); + + // Display an error if the form markup couldn't be generated; + // otherwise, render the HTML. + if (Auth_OpenID::isFailure($form_html)) { + displayError("Could not redirect to server: " . $form_html->message); + } else { + print $form_html; + } + } +} + +run(); + +?>
\ No newline at end of file diff --git a/examples/detect.php b/examples/detect.php new file mode 100644 index 0000000..2d06ae1 --- /dev/null +++ b/examples/detect.php @@ -0,0 +1,517 @@ +<?php + +$path_extra = dirname(dirname(__FILE__)); +$path = ini_get('include_path'); +$path = $path_extra . PATH_SEPARATOR . $path; +ini_set('include_path', $path); + +define('IS_WINDOWS', strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'); + +class PlainText { + function start($title) + { + return ''; + } + + function tt($text) + { + return $text; + } + + function link($href, $text=null) + { + if ($text) { + return $text . ' <' . $href . '>'; + } else { + return $href; + } + } + + function b($text) + { + return '*' . $text . '*'; + } + + function contentType() + { + return 'text/plain'; + } + + function p($text) + { + return wordwrap($text) . "\n\n"; + } + + function pre($text) + { + $out = ''; + $lines = array_map('trim', explode("\n", $text)); + foreach ($lines as $line) { + $out .= ' ' . $line . "\n"; + } + $out .= "\n"; + return $out; + } + + function ol($items) + { + $out = ''; + $c = 1; + foreach ($items as $item) { + $item = wordwrap($item, 72); + $lines = array_map('trim', explode("\n", $item)); + $out .= $c . '. ' . $lines[0] . "\n"; + unset($lines[0]); + foreach ($lines as $line) { + $out .= ' ' . $line . "\n"; + } + $out .= "\n"; + $c += 1; + } + return $out; + } + + function h2($text) + { + return $this->h($text, 2); + } + + function h1($text) + { + return $this->h($text, 1); + } + + function h($text, $n) + { + $chars = '#=+-.'; + $c = $chars[$n - 1]; + return "\n" . $text . "\n" . str_repeat($c, strlen($text)) . "\n\n"; + } + + function end() + { + return ''; + } +} + +class HTML { + function start($title) + { + return '<html><head><title>' . $title . '</title>' . + $this->stylesheet(). + '</head><body>' . "\n"; + } + + function stylesheet() + { + return "<style type='text/css'>\n". + "p {\n". + " width: 50em;\n". + "}\n". + '</style>'; + } + + function tt($text) + { + return '<code>' . $text . '</code>'; + } + + function contentType() + { + return 'text/html'; + } + + function b($text) + { + return '<strong>' . $text . '</strong>'; + } + + function p($text) + { + return '<p>' . wordwrap($text) . "</p>\n"; + } + + function pre($text) + { + return '<pre>' . $text . "</pre>\n"; + } + + function ol($items) + { + $out = '<ol>'; + foreach ($items as $item) { + $out .= '<li>' . wordwrap($item) . "</li>\n"; + } + $out .= "</ol>\n"; + return $out; + } + + function h($text, $n) + { + return "<h$n>$text</h$n>\n"; + } + + function h2($text) + { + return $this->h($text, 2); + } + + function h1($text) + { + return $this->h($text, 1); + } + + function link($href, $text=null) + { + return '<a href="' . $href . '">' . ($text ? $text : $href) . '</a>'; + } + + function end() + { + return "</body>\n</html>\n"; + } +} + +if (isset($_SERVER['REQUEST_METHOD'])) { + $r = new HTML(); +} else { + $r = new PlainText(); +} + +function detect_math($r, &$out) +{ + $out .= $r->h2('Math support'); + $ext = Auth_OpenID_detectMathLibrary(Auth_OpenID_math_extensions()); + if (!isset($ext['extension']) || !isset($ext['class'])) { + $out .= $r->p( + 'Your PHP installation does not include big integer math ' . + 'support. This support is required if you wish to run a ' . + 'secure OpenID server without using SSL.'); + $out .= $r->p('To use this library, you have a few options:'); + + $gmp_lnk = $r->link('http://www.php.net/manual/en/ref.gmp.php', 'GMP'); + $bc_lnk = $r->link('http://www.php.net/manual/en/ref.bc.php', 'bcmath'); + $out .= $r->ol(array( + 'Install the ' . $gmp_lnk . ' PHP extension', + 'Install the ' . $bc_lnk . ' PHP extension', + 'If your site is low-security, call ' . + 'Auth_OpenID_setNoMathSupport(), defined in Auth/OpenID/BigMath.php. ', + 'The library will function, but ' . + 'the security of your OpenID server will depend on the ' . + 'security of the network links involved. If you are only ' . + 'using consumer support, you should still be able to operate ' . + 'securely when the users are communicating with a ' . + 'well-implemented server.')); + return false; + } else { + switch ($ext['extension']) { + case 'bcmath': + $out .= $r->p('Your PHP installation has bcmath support. This is ' . + 'adequate for small-scale use, but can be CPU-intensive. ' . + 'You may want to look into installing the GMP extension.'); + $lnk = $r->link('http://www.php.net/manual/en/ref.gmp.php'); + $out .= $r->p('See ' . $lnk .' for more information ' . + 'about the GMP extension.'); + break; + case 'gmp': + $out .= $r->p('Your PHP installation has gmp support. Good.'); + break; + default: + $class = $ext['class']; + $lib = new $class(); + $one = $lib->init(1); + $two = $lib->add($one, $one); + $t = $lib->toString($two); + $out .= $r->p('Uh-oh. I do not know about the ' . + $ext['extension'] . ' extension!'); + if ($t != '2') { + $out .= $r->p('It looks like it is broken. 1 + 1 = ' . + var_export($t, false)); + return false; + } else { + $out .= $r->p('But it seems to be able to add one and one.'); + } + } + return true; // Math library is OK + } +} + +function detect_random($r, &$out) +{ + $out .= $r->h2('Cryptographic-quality randomness source'); + if (Auth_OpenID_RAND_SOURCE === null) { + $out .= $r->p('Using (insecure) pseudorandom number source, because ' . + 'Auth_OpenID_RAND_SOURCE has been defined as null.'); + return false; + } + + $msg = 'The library will try to access ' . Auth_OpenID_RAND_SOURCE + . ' as a source of random data. '; + + $numbytes = 6; + + $f = @fopen(Auth_OpenID_RAND_SOURCE, 'r'); + if ($f !== false) { + $data = fread($f, $numbytes); + $stat = fstat($f); + $size = $stat['size']; + fclose($f); + } else { + $data = null; + $size = true; + } + + if ($f !== false) { + $dataok = (Auth_OpenID::bytes($data) == $numbytes); + $ok = $dataok && !$size; + $msg .= 'It seems to exist '; + if ($dataok) { + $msg .= 'and be readable. Here is some hex data: ' . + bin2hex($data) . '.'; + } else { + $msg .= 'but reading data failed.'; + } + if ($size) { + $msg .= ' This is a ' . $size . ' byte file. Unless you know ' . + 'what you are doing, it is likely that you are making a ' . + 'mistake by using a regular file as a randomness source.'; + } + } else { + $msg .= Auth_OpenID_RAND_SOURCE . + ' could not be opened. This could be because of restrictions on' . + ' your PHP environment or that randomness source may not exist' . + ' on this platform.'; + if (IS_WINDOWS) { + $msg .= ' You seem to be running Windows. This library does not' . + ' have access to a good source of randomness on Windows.'; + } + $ok = false; + } + + $out .= $r->p($msg); + + if (!$ok) { + $out .= $r->p( + 'To set a source of randomness, define Auth_OpenID_RAND_SOURCE ' . + 'to the path to the randomness source. If your platform does ' . + 'not provide a secure randomness source, the library can' . + 'operate in pseudorandom mode, but it is then vulnerable to ' . + 'theoretical attacks. If you wish to operate in pseudorandom ' . + 'mode, define Auth_OpenID_RAND_SOURCE to null.'); + $out .= $r->p('You are running on:'); + $out .= $r->pre(php_uname()); + $out .= $r->p('There does not seem to be an available source ' . + 'of randomness. On a Unix-like platform ' . + '(including MacOS X), try /dev/random and ' . + '/dev/urandom.'); + } + return $ok; +} + +function detect_stores($r, &$out) +{ + $out .= $r->h2('Data storage'); + + $found = array(); + foreach (array('sqlite', 'mysql', 'pgsql') as $dbext) { + if (extension_loaded($dbext) || @dl($dbext . '.' . PHP_SHLIB_SUFFIX)) { + $found[] = $dbext; + } + } + if (count($found) == 0) { + $text = 'No SQL database support was found in this PHP ' . + 'installation. See the PHP manual if you need to ' . + 'use an SQL database.'; + } else { + $text = 'Support was found for '; + if (count($found) == 1) { + $text .= $found[0] . '.'; + } else { + $last = array_pop($found); + $text .= implode(', ', $found) . ' and ' . $last . '.'; + } + $text = $r->b($text); + } + $text .= ' The library supports the MySQL, PostgreSQL, and SQLite ' . + 'database engines, as well as filesystem-based storage. In ' . + 'addition, PEAR DB is required to use databases.'; + $out .= $r->p($text); + + if (function_exists('posix_getpwuid') && + function_exists('posix_geteuid')) { + $processUser = posix_getpwuid(posix_geteuid()); + $web_user = $r->b($r->tt($processUser['name'])); + } else { + $web_user = 'the PHP process'; + } + + if (in_array('sqlite', $found)) { + $out .= $r->p('If you are using SQLite, your database must be ' . + 'writable by ' . $web_user . ' and not available over' . + ' the web.'); + } + + $basedir_str = ini_get('open_basedir'); + if (gettype($basedir_str) == 'string') { + $url = 'http://www.php.net/manual/en/features.safe-mode.php' . + '#ini.open-basedir'; + $lnk = $r->link($url, 'open_basedir'); + $out .= $r->p('If you are using a filesystem-based store or SQLite, ' . + 'be aware that ' . $lnk . ' is in effect. This means ' . + 'that your data will have to be stored in one of the ' . + 'following locations:'); + $out .= $r->pre(var_export($basedir_str, true)); + } else { + $out .= $r->p('The ' . $r->b($r->tt('open_basedir')) . ' configuration restriction ' . + 'is not in effect.'); + } + + $out .= $r->p('If you are using the filesystem store, your ' . + 'data directory must be readable and writable by ' . + $web_user . ' and not availabe over the Web.'); + return true; +} + +function detect_xml($r, &$out) +{ + global $__Auth_Yadis_xml_extensions; + + $out .= $r->h2('XML Support'); + + // Try to get an XML extension. + $ext = Auth_Yadis_getXMLParser(); + + if ($ext !== null) { + $out .= $r->p('XML parsing support is present using the '. + $r->b(get_class($ext)).' interface.'); + return true; + } else { + $out .= $r->p('XML parsing support is absent; please install one '. + 'of the following PHP extensions:'); + foreach ($__Auth_Yadis_xml_extensions as $name => $cls) { + $out .= "<li>" . $r->b($name) . "</li>"; + } + return false; + } +} + +function detect_fetcher($r, &$out) +{ + $out .= $r->h2('HTTP Fetching'); + + $result = @include 'Auth/Yadis/Yadis.php'; + + if (!$result) { + $out .= $r->p('Yadis code unavailable; could not test fetcher support.'); + return false; + } + + if (Auth_Yadis_Yadis::curlPresent()) { + $out .= $r->p('This PHP installation has support for libcurl. Good.'); + } else { + $out .= $r->p('This PHP installation does not have support for ' . + 'libcurl. CURL is not required but is recommended. '. + 'The OpenID library will use an fsockopen()-based fetcher.'); + $lnk = $r->link('http://us3.php.net/manual/en/ref.curl.php'); + $out .= $r->p('See ' . $lnk . ' about enabling the libcurl support ' . + 'for PHP.'); + } + + $ok = true; + $fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); + $fetch_url = 'http://www.openidenabled.com/resources/php-fetch-test'; + $expected_url = $fetch_url . '.txt'; + $result = $fetcher->get($fetch_url); + + if (isset($result)) { + $parts = array('An HTTP request was completed.'); + // list ($code, $url, $data) = $result; + if ($result->status != '200' && $result->status != '206') { + $ok = false; + $parts[] = $r->b( + sprintf( + 'Got %s instead of the expected HTTP status ' . + 'code (200 or 206).', $result->status)); + } + + $url = $result->final_url; + if ($url != $expected_url) { + $ok = false; + if ($url == $fetch_url) { + $msg = 'The redirected URL was not returned.'; + } else { + $msg = 'An unexpected URL was returned: <' . $url . '>.'; + } + $parts[] = $r->b($msg); + } + + $data = $result->body; + if ($data != 'Hello World!') { + $ok = false; + $parts[] = $r->b('Unexpected data was returned.'); + } + $out .= $r->p(implode(' ', $parts)); + } else { + $ok = false; + $out .= $r->p('Fetching URL ' . $lnk . ' failed!'); + } + + if ($fetcher->supportsSSL()) { + $out .= $r->p('Your PHP installation appears to support SSL, so it ' . + 'will be able to process HTTPS identity URLs and server URLs.'); + } else { + $out .= $r->p('Your PHP installation does not support SSL, so it ' . + 'will NOT be able to process HTTPS identity URLs and server URLs.'); + } + + return $ok; +} + +header('Content-Type: ' . $r->contentType() . '; charset=us-ascii'); + +$title = 'OpenID Library Support Report'; +$out = $r->start($title) . + $r->h1($title) . + $r->p('This script checks your PHP installation to determine if you ' . + 'are set up to use the JanRain PHP OpenID library.'); + +$body = ''; + +$_include = include 'Auth/OpenID.php'; + +if (!$_include) { + $path = ini_get('include_path'); + $body .= $r->p( + 'Cannot find the OpenID library. It must be in your PHP include ' . + 'path. Your PHP include path is currently:'); + $body .= $r->pre($path); +} else { + $status = array(); + + $status[] = detect_math($r, $body); + $status[] = detect_random($r, $body); + $status[] = detect_stores($r, $body); + $status[] = detect_fetcher($r, $body); + $status[] = detect_xml($r, $body); + + $result = true; + + foreach ($status as $v) { + if (!$v) { + $result = false; + break; + } + } + + if ($result) { + $out .= $r->h2('Setup Complete!'); + $out .= $r->p('Your system should be ready to run the OpenID library.'); + } else { + $out .= $r->h2('Setup Incomplete'); + $out .= $r->p('Your system needs a few changes before it will be ready to run the OpenID library.'); + } +} + +$out .= $body . $r->end(); +print $out; +?> diff --git a/examples/discover.php b/examples/discover.php new file mode 100644 index 0000000..31e6b61 --- /dev/null +++ b/examples/discover.php @@ -0,0 +1,100 @@ +<?php + +require_once "consumer/common.php"; + +require_once "Auth/OpenID/Discover.php"; +require_once "Auth/Yadis/Yadis.php"; + +function getOpenIDIdentifier() +{ + return $_GET['openid_identifier']; +} + +function escape($x) +{ + return htmlentities($x); +} + + +$identifier = getOpenIDIdentifier(); +?> +<html> +<head> +<title>OpenID discovery</title> +</head> +<body> + <h2>OpenID discovery tool</h2> + <p> + Enter an OpenID URL to begin discovery: + </p> + <form> + <input type="text" name="openid_identifier" size="40" /> + <input type="submit" value="Begin" /> + </form> +<? +if ($identifier) { + + $fetcher = Auth_Yadis_Yadis::getHTTPFetcher(); + list($normalized_identifier, $endpoints) = Auth_OpenID_discover( + $identifier, $fetcher); + +?> + <h3>Discovery Results for <?= escape($identifier) ?></h3> + + <table cellpadding="7" cellspacing="0"> + <tbody> + <tr> + <th>Claimed Identifier</th> + <td><?= escape($normalized_identifier) ?></td> + </tr> +<? +if (!$endpoints) { +?> + <tr> + <td colspan="2">No OpenID services discovered.</td> + </tr> +<? +} else { +?> + <tr> + <td colspan="2">Discovered OpenID services:</td> + </tr> +<? +foreach ($endpoints as $endpoint) { +?> + <tr> + <td colspan="2"><hr/></td> + </tr> + <tr> + <th>Server URL</th> + <td><tt><?= escape($endpoint->server_url) ?></tt></td> + </tr> + <tr> + <th>Local ID</th> + <td><tt><?= escape($endpoint->local_id) ?></tt></td> + </tr> + <tr> + <td colspan="2"> + <h3>Service types:</h3> + <ul> +<? +foreach ($endpoint->type_uris as $type_uri) { +?> + <li><tt><?= escape($type_uri) ?></tt></li> +<? +} +?> + </ul> + </td> + </tr> +<? +} +} +?> + </tbody> +</table> +<? +} +?> +</body> +</html>
\ No newline at end of file diff --git a/examples/server/index.php b/examples/server/index.php new file mode 100644 index 0000000..7a95064 --- /dev/null +++ b/examples/server/index.php @@ -0,0 +1,5 @@ +<?php + +header("Location: server.php"); + +?>
\ No newline at end of file diff --git a/examples/server/lib/actions.php b/examples/server/lib/actions.php new file mode 100644 index 0000000..50dc19a --- /dev/null +++ b/examples/server/lib/actions.php @@ -0,0 +1,164 @@ +<?php + +require_once "lib/common.php"; +require_once "lib/session.php"; +require_once "lib/render.php"; + +require_once "lib/render/login.php"; +require_once "lib/render/idpage.php"; +require_once "lib/render/idpXrds.php"; +require_once "lib/render/userXrds.php"; + +require_once "Auth/OpenID.php"; + +/** + * Handle a standard OpenID server request + */ +function action_default() +{ + header('X-XRDS-Location: '.buildURL('idpXrds')); + + $server =& getServer(); + $method = $_SERVER['REQUEST_METHOD']; + $request = null; + if ($method == 'GET') { + $request = $_GET; + } else { + $request = $_POST; + } + + $request = $server->decodeRequest(); + + if (!$request) { + return about_render(); + } + + setRequestInfo($request); + + if (in_array($request->mode, + array('checkid_immediate', 'checkid_setup'))) { + + if ($request->idSelect()) { + // Perform IDP-driven identifier selection + if ($request->mode == 'checkid_immediate') { + $response =& $request->answer(false); + } else { + return trust_render($request); + } + } else if ((!$request->identity) && + (!$request->idSelect())) { + // No identifier used or desired; display a page saying + // so. + return noIdentifier_render(); + } else if ($request->immediate) { + $response =& $request->answer(false, buildURL()); + } else { + if (!getLoggedInUser()) { + return login_render(); + } + return trust_render($request); + } + } else { + $response =& $server->handleRequest($request); + } + + $webresponse =& $server->encodeResponse($response); + + if ($webresponse->code != AUTH_OPENID_HTTP_OK) { + header(sprintf("HTTP/1.1 %d ", $webresponse->code), + true, $webresponse->code); + } + + foreach ($webresponse->headers as $k => $v) { + header("$k: $v"); + } + + header(header_connection_close); + print $webresponse->body; + exit(0); +} + +/** + * Log out the currently logged in user + */ +function action_logout() +{ + setLoggedInUser(null); + setRequestInfo(null); + return authCancel(null); +} + +/** + * Check the input values for a login request + */ +function login_checkInput($input) +{ + $openid_url = false; + $errors = array(); + + if (!isset($input['openid_url'])) { + $errors[] = 'Enter an OpenID URL to continue'; + } + if (count($errors) == 0) { + $openid_url = $input['openid_url']; + } + return array($errors, $openid_url); +} + +/** + * Log in a user and potentially continue the requested identity approval + */ +function action_login() +{ + $method = $_SERVER['REQUEST_METHOD']; + switch ($method) { + case 'GET': + return login_render(); + case 'POST': + $info = getRequestInfo(); + $fields = $_POST; + if (isset($fields['cancel'])) { + return authCancel($info); + } + + list ($errors, $openid_url) = login_checkInput($fields); + if (count($errors) || !$openid_url) { + $needed = $info ? $info->identity : false; + return login_render($errors, @$fields['openid_url'], $needed); + } else { + setLoggedInUser($openid_url); + return doAuth($info); + } + default: + return login_render(array('Unsupported HTTP method: $method')); + } +} + +/** + * Ask the user whether he wants to trust this site + */ +function action_trust() +{ + $info = getRequestInfo(); + $trusted = isset($_POST['trust']); + return doAuth($info, $trusted, true, @$_POST['idSelect']); +} + +function action_idpage() +{ + $identity = $_GET['user']; + return idpage_render($identity); +} + +function action_idpXrds() +{ + return idpXrds_render(); +} + +function action_userXrds() +{ + $identity = $_GET['user']; + return userXrds_render($identity); +} + +?>
\ No newline at end of file diff --git a/examples/server/lib/common.php b/examples/server/lib/common.php new file mode 100644 index 0000000..80d05f5 --- /dev/null +++ b/examples/server/lib/common.php @@ -0,0 +1,95 @@ +<?php + +require_once "lib/render.php"; +require_once "lib/session.php"; + +require_once "lib/render/login.php"; +require_once "lib/render/about.php"; +require_once "lib/render/trust.php"; + +require_once "Auth/OpenID/Server.php"; +require_once "Auth/OpenID/SReg.php"; + +function authCancel($info) +{ + if ($info) { + setRequestInfo(); + $url = $info->getCancelURL(); + } else { + $url = getServerURL(); + } + return redirect_render($url); +} + +function doAuth($info, $trusted=null, $fail_cancels=false, + $idpSelect=null) +{ + if (!$info) { + // There is no authentication information, so bail + return authCancel(null); + } + + if ($info->idSelect()) { + if ($idpSelect) { + $req_url = idURL($idpSelect); + } else { + $trusted = false; + } + } else { + $req_url = $info->identity; + } + + $user = getLoggedInUser(); + setRequestInfo($info); + + if ((!$info->idSelect()) && ($req_url != idURL($user))) { + return login_render(array(), $req_url, $req_url); + } + + $trust_root = $info->trust_root; + + if ($trusted) { + setRequestInfo(); + $server =& getServer(); + $response =& $info->answer(true, null, $req_url); + + // Answer with some sample Simple Registration data. + $sreg_data = array( + 'fullname' => 'Example User', + 'nickname' => 'example', + 'dob' => '1970-01-01', + 'email' => 'invalid@example.com', + 'gender' => 'F', + 'postcode' => '12345', + 'country' => 'ES', + 'language' => 'eu', + 'timezone' => 'America/New_York'); + + // Add the simple registration response values to the OpenID + // response message. + $sreg_request = Auth_OpenID_SRegRequest::fromOpenIDRequest( + $info); + + $sreg_response = Auth_OpenID_SRegResponse::extractResponse( + $sreg_request, $sreg_data); + + $sreg_response->toMessage($response->fields); + + // Generate a response to send to the user agent. + $webresponse =& $server->encodeResponse($response); + + $new_headers = array(); + + foreach ($webresponse->headers as $k => $v) { + $new_headers[] = $k.": ".$v; + } + + return array($new_headers, $webresponse->body); + } elseif ($fail_cancels) { + return authCancel($info); + } else { + return trust_render($info); + } +} + +?>
\ No newline at end of file diff --git a/examples/server/lib/render.php b/examples/server/lib/render.php new file mode 100644 index 0000000..33d2aef --- /dev/null +++ b/examples/server/lib/render.php @@ -0,0 +1,114 @@ +<?php + +define('page_template', +'<html> + <head> + <meta http-equiv="cache-control" content="no-cache"/> + <meta http-equiv="pragma" content="no-cache"/> + <title>%s</title> +%s + </head> + <body> + %s +<div id="content"> + <h1>%s</h1> + %s +</div> + </body> +</html>'); + +define('logged_in_pat', 'You are logged in as %s (URL: %s)'); + +/** + * HTTP response line contstants + */ +define('http_bad_request', 'HTTP/1.1 400 Bad Request'); +define('http_found', 'HTTP/1.1 302 Found'); +define('http_ok', 'HTTP/1.1 200 OK'); +define('http_internal_error', 'HTTP/1.1 500 Internal Error'); + +/** + * HTTP header constants + */ +define('header_connection_close', 'Connection: close'); +define('header_content_text', 'Content-Type: text/plain; charset=us-ascii'); + +define('redirect_message', + 'Please wait; you are being redirected to <%s>'); + + +/** + * Return a string containing an anchor tag containing the given URL + * + * The URL does not need to be quoted, but if text is passed in, then + * it does. + */ +function link_render($url, $text=null) { + $esc_url = htmlspecialchars($url, ENT_QUOTES); + $text = ($text === null) ? $esc_url : $text; + return sprintf('<a href="%s">%s</a>', $esc_url, $text); +} + +/** + * Return an HTTP redirect response + */ +function redirect_render($redir_url) +{ + $headers = array(http_found, + header_content_text, + header_connection_close, + 'Location: ' . $redir_url, + ); + $body = sprintf(redirect_message, $redir_url); + return array($headers, $body); +} + +function navigation_render($msg, $items) +{ + $what = link_render(buildURL(), 'PHP OpenID Server'); + if ($msg) { + $what .= ' — ' . $msg; + } + if ($items) { + $s = '<p>' . $what . '</p><ul class="bottom">'; + foreach ($items as $action => $text) { + $url = buildURL($action); + $s .= sprintf('<li>%s</li>', link_render($url, $text)); + } + $s .= '</ul>'; + } else { + $s = '<p class="bottom">' . $what . '</p>'; + } + return sprintf('<div class="navigation">%s</div>', $s); +} + +/** + * Render an HTML page + */ +function page_render($body, $user, $title, $h1=null, $login=false) +{ + $h1 = $h1 ? $h1 : $title; + + if ($user) { + $msg = sprintf(logged_in_pat, link_render(idURL($user), $user), + link_render(idURL($user))); + $nav = array('logout' => 'Log Out'); + + $navigation = navigation_render($msg, $nav); + } else { + if (!$login) { + $msg = link_render(buildURL('login'), 'Log In'); + $navigation = navigation_render($msg, array()); + } else { + $navigation = ''; + } + } + + $style = getStyle(); + $text = sprintf(page_template, $title, $style, $navigation, $h1, $body); + // No special headers here + $headers = array(); + return array($headers, $text); +} + +?>
\ No newline at end of file diff --git a/examples/server/lib/render/about.php b/examples/server/lib/render/about.php new file mode 100644 index 0000000..bd35836 --- /dev/null +++ b/examples/server/lib/render/about.php @@ -0,0 +1,58 @@ +<?php + +require_once "lib/session.php"; +require_once "lib/render.php"; + +define('about_error_template', + '<div class="error"> +An error occurred when processing your request: +<br /> +%s +</div>'); + +define('about_body', + '<p> + This is an <a href="http://www.openid.net/">OpenID</a> server + endpoint. This server is built on the <a + href="http://www.openidenabled.com/openid/libraries/php">JanRain PHP OpenID + library</a>. Since OpenID consumer sites will need to directly contact this + server, it must be accessible over the Internet (not behind a firewall). +</p> +<p> + To use this server, you will have to set up a URL to use as an identifier. + Insert the following markup into the <code><head></code> of the HTML + document at that URL: +</p> +<pre><link rel="openid.server" href="%s" /></pre> +<p> + Then configure this server so that you can log in with that URL. Once you + have configured the server, and marked up your identity URL, you can verify + that it is working by using the <a href="http://www.openidenabled.com/" + >openidenabled.com</a> + <a href="http://www.openidenabled.com/resources/openid-test/checkup">OpenID + Checkup tool</a>: + <form method="post" + action="http://www.openidenabled.com/resources/openid-test/checkup/start"> + <label for="checkup">OpenID URL: + </label><input id="checkup" type="text" name="openid_url" /> + <input type="submit" value="Check" /> + </form> +</p> +'); + +/** + * Render the about page, potentially with an error message + */ +function about_render($error=false, $internal=true) +{ + $headers = array(); + $body = sprintf(about_body, buildURL()); + if ($error) { + $headers[] = $internal ? http_internal_error : http_bad_request; + $body .= sprintf(about_error_template, htmlspecialchars($error)); + } + $current_user = getLoggedInUser(); + return page_render($body, $current_user, 'OpenID Server Endpoint'); +} + +?>
\ No newline at end of file diff --git a/examples/server/lib/render/idpXrds.php b/examples/server/lib/render/idpXrds.php new file mode 100644 index 0000000..6e4ae1c --- /dev/null +++ b/examples/server/lib/render/idpXrds.php @@ -0,0 +1,32 @@ +<?php + +require_once "lib/session.php"; +require_once "lib/render.php"; + +require_once "Auth/OpenID/Discover.php"; + +define('idp_xrds_pat', '<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS + xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)"> + <XRD> + <Service priority="0"> + <Type>%s</Type> + <URI>%s</URI> + </Service> + </XRD> +</xrds:XRDS> +'); + +function idpXrds_render() +{ + $headers = array('Content-type: application/xrds+xml'); + + $body = sprintf(idp_xrds_pat, + Auth_OpenID_TYPE_2_0_IDP, + buildURL()); + + return array($headers, $body); +} + +?>
\ No newline at end of file diff --git a/examples/server/lib/render/idpage.php b/examples/server/lib/render/idpage.php new file mode 100644 index 0000000..48c2486 --- /dev/null +++ b/examples/server/lib/render/idpage.php @@ -0,0 +1,31 @@ +<?php + +require_once "lib/session.php"; +require_once "lib/render.php"; + +define('idpage_pat', + '<html> +<head> + <link rel="openid2.provider openid.server" href="%s"/> + <meta http-equiv="X-XRDS-Location" content="%s" /> +</head> +<body> + This is the identity page for users of this server. +</body> +</html>'); + +function idpage_render($identity) +{ + $xrdsurl = buildURL('userXrds')."?user=".urlencode($identity); + + $headers = array( + 'X-XRDS-Location: '.$xrdsurl); + + + $body = sprintf(idpage_pat, + buildURL(), + $xrdsurl); + return array($headers, $body); +} + +?> diff --git a/examples/server/lib/render/login.php b/examples/server/lib/render/login.php new file mode 100644 index 0000000..986a885 --- /dev/null +++ b/examples/server/lib/render/login.php @@ -0,0 +1,65 @@ +<?php + +require_once "lib/session.php"; +require_once "lib/render.php"; + +define('login_form_pat', + '<div class="form"> + <p> + + Enter your username into this form to log in to this server. It + can be anything; this is just for demonstration purposes. For + example, entering USERNAME will give you the identity URL + + <pre>%s</pre> + </p> + + <form method="post" action="%s"> + <table> + <tr> + <th><label for="openid_url">Name:</label></th> + <td><input type="text" name="openid_url" + value="%s" id="openid_url" /></td> + </tr> + <tr> + <td colspan="2"> + <input type="submit" value="Log in" /> + <input type="submit" name="cancel" value="Cancel" /> + </td> + </tr> + </table> + </form> +</div> +'); + +define('login_needed_pat', + 'You must be logged in as %s to approve this request.'); + +function login_render($errors=null, $input=null, $needed=null) +{ + $current_user = getLoggedInUser(); + if ($input === null) { + $input = $current_user; + } + if ($needed) { + $errors[] = sprintf(login_needed_pat, link_render($needed)); + } + + $esc_input = htmlspecialchars($input, ENT_QUOTES); + $login_url = buildURL('login', true); + $body = sprintf(login_form_pat, idURL('USERNAME'), $login_url, $esc_input); + if ($errors) { + $body = loginError_render($errors) . $body; + } + return page_render($body, $current_user, 'Log In', null, true); +} + +function loginError_render($errors) +{ + $text = ''; + foreach ($errors as $error) { + $text .= sprintf("<li>%s</li>\n", $error); + } + return sprintf("<ul class=\"error\">\n%s</ul>\n", $text); +} +?>
\ No newline at end of file diff --git a/examples/server/lib/render/trust.php b/examples/server/lib/render/trust.php new file mode 100644 index 0000000..681d456 --- /dev/null +++ b/examples/server/lib/render/trust.php @@ -0,0 +1,56 @@ +<?php + +require_once "lib/session.php"; +require_once "lib/render.php"; + +define('trust_form_pat', + '<div class="form"> + <form method="post" action="%s"> + %s + <input type="submit" name="trust" value="Confirm" /> + <input type="submit" value="Do not confirm" /> + </form> +</div> +'); + +define('normal_pat', + '<p>Do you wish to confirm your identity ' . + '(<code>%s</code>) with <code>%s</code>?</p>'); + +define('id_select_pat', + '<p>You entered the server URL at the RP. +Please choose the name you wish to use. If you enter nothing, the request will be cancelled.<br/> +<input type="text" name="idSelect" /></p> +'); + +define('no_id_pat', +' +You did not send an identifier with the request, +and it was not an identifier selection request. +Please return to the relying party and try again. +'); + +function trust_render($info) +{ + $current_user = getLoggedInUser(); + $lnk = link_render(idURL($current_user)); + $trust_root = htmlspecialchars($info->trust_root); + $trust_url = buildURL('trust', true); + + if ($info->idSelect()) { + $prompt = id_select_pat; + } else { + $prompt = sprintf(normal_pat, $lnk, $trust_root); + } + + $form = sprintf(trust_form_pat, $trust_url, $prompt); + + return page_render($form, $current_user, 'Trust This Site'); +} + +function noIdentifier_render() +{ + return page_render(no_id_pat, null, 'No Identifier Sent'); +} + +?>
\ No newline at end of file diff --git a/examples/server/lib/render/userXrds.php b/examples/server/lib/render/userXrds.php new file mode 100644 index 0000000..a9ea95e --- /dev/null +++ b/examples/server/lib/render/userXrds.php @@ -0,0 +1,34 @@ +<?php + +require_once "lib/session.php"; +require_once "lib/render.php"; + +require_once "Auth/OpenID/Discover.php"; + +define('user_xrds_pat', '<?xml version="1.0" encoding="UTF-8"?> +<xrds:XRDS + xmlns:xrds="xri://$xrds" + xmlns="xri://$xrd*($v*2.0)"> + <XRD> + <Service priority="0"> + <Type>%s</Type> + <Type>%s</Type> + <URI>%s</URI> + </Service> + </XRD> +</xrds:XRDS> +'); + +function userXrds_render($identity) +{ + $headers = array('Content-type: application/xrds+xml'); + + $body = sprintf(user_xrds_pat, + Auth_OpenID_TYPE_2_0, + Auth_OpenID_TYPE_1_1, + buildURL()); + + return array($headers, $body); +} + +?>
\ No newline at end of file diff --git a/examples/server/lib/session.php b/examples/server/lib/session.php new file mode 100644 index 0000000..201b6ee --- /dev/null +++ b/examples/server/lib/session.php @@ -0,0 +1,178 @@ +<?php + +require_once "config.php"; +require_once "lib/render.php"; +require_once "Auth/OpenID/Server.php"; + +/** + * Set up the session + */ +function init() +{ + session_name('openid_server'); + session_start(); +} + +/** + * Get the style markup + */ +function getStyle() +{ + $parent = rtrim(dirname(getServerURL()), '/'); + $url = htmlspecialchars($parent . '/openid-server.css', ENT_QUOTES); + return sprintf('<link rel="stylesheet" type="text/css" href="%s" />', $url); +} + +/** + * Get the URL of the current script + */ +function getServerURL() +{ + $path = $_SERVER['SCRIPT_NAME']; + $host = $_SERVER['HTTP_HOST']; + $port = $_SERVER['SERVER_PORT']; + $s = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] ? 's' : ''; + if (($s && $port == "443") || (!$s && $port == "80")) { + $p = ''; + } else { + $p = ':' . $port; + } + + return "http$s://$host$p$path"; +} + +/** + * Build a URL to a server action + */ +function buildURL($action=null, $escaped=true) +{ + $url = getServerURL(); + if ($action) { + $url .= '/' . $action; + } + return $escaped ? htmlspecialchars($url, ENT_QUOTES) : $url; +} + +/** + * Extract the current action from the request + */ +function getAction() +{ + $path_info = @$_SERVER['PATH_INFO']; + $action = ($path_info) ? substr($path_info, 1) : ''; + $function_name = 'action_' . $action; + return $function_name; +} + +/** + * Write the response to the request + */ +function writeResponse($resp) +{ + list ($headers, $body) = $resp; + array_walk($headers, 'header'); + header(header_connection_close); + print $body; +} + +/** + * Instantiate a new OpenID server object + */ +function getServer() +{ + static $server = null; + if (!isset($server)) { + $server =& new Auth_OpenID_Server(getOpenIDStore(), + buildURL()); + } + return $server; +} + +/** + * Return a hashed form of the user's password + */ +function hashPassword($password) +{ + return bin2hex(Auth_OpenID_SHA1($password)); +} + +/** + * Get the openid_url out of the cookie + * + * @return mixed $openid_url The URL that was stored in the cookie or + * false if there is none present or if the cookie is bad. + */ +function getLoggedInUser() +{ + return isset($_SESSION['openid_url']) + ? $_SESSION['openid_url'] + : false; +} + +/** + * Set the openid_url in the cookie + * + * @param mixed $identity_url The URL to set. If set to null, the + * value will be unset. + */ +function setLoggedInUser($identity_url=null) +{ + if (!isset($identity_url)) { + unset($_SESSION['openid_url']); + } else { + $_SESSION['openid_url'] = $identity_url; + } +} + +function getRequestInfo() +{ + return isset($_SESSION['request']) + ? unserialize($_SESSION['request']) + : false; +} + +function setRequestInfo($info=null) +{ + if (!isset($info)) { + unset($_SESSION['request']); + } else { + $_SESSION['request'] = serialize($info); + } +} + + +function getSreg($identity) +{ + // from config.php + global $openid_sreg; + + if (!is_array($openid_sreg)) { + return null; + } + + return $openid_sreg[$identity]; + +} + +function idURL($identity) +{ + return buildURL('idpage') . "?user=" . $identity; +} + +function idFromURL($url) +{ + if (strpos($url, 'idpage') === false) { + return null; + } + + $parsed = parse_url($url); + + $q = $parsed['query']; + + $parts = array(); + parse_str($q, $parts); + + return @$parts['user']; +} + +?>
\ No newline at end of file diff --git a/examples/server/openid-server.css b/examples/server/openid-server.css new file mode 100644 index 0000000..badff23 --- /dev/null +++ b/examples/server/openid-server.css @@ -0,0 +1,74 @@ +body { + padding: 0; + margin: 0; +} + +#content { + padding: 0.5em; + max-width: 50em; +} + +ul.error { + background: #ffaaaa; + border: 1px solid #ff0000; + padding: 0.5em; + padding-left: 1.5em; +} + +.login th { + text-align: left; +} + +div.form { + border: thin solid #777777; + background: #dddddd; + padding: 0.5em; + margin-top: 1em; +} + +div.navigation { + border-bottom: thin solid #cccccc; + background: #eeeeee; + font-size: smaller; + padding: 0.5em; +} + +div.navigation h2 { + margin-top: 0; +} + +div.navigation p { + margin: 0; +} + +div.navigation ul { + margin: 0; +} + +div.login p { + margin-top: 0; +} + +h1 { + margin-top: 0; +} + +pre { + padding: 1em; + border: 1px solid black; + background: #ffeebb; +} + +#checkup { + background: url('http://www.openidenabled.com/favicon.ico') no-repeat; + padding-left: 16px; +} + +th { + text-align: left; +} + +table { + border-collapse: collapse; + margin-bottom: 1em; +}
\ No newline at end of file diff --git a/examples/server/server.php b/examples/server/server.php new file mode 100644 index 0000000..f054be8 --- /dev/null +++ b/examples/server/server.php @@ -0,0 +1,48 @@ +<?php + +$path_extra = dirname(dirname(dirname(__FILE__))); +$path = ini_get('include_path'); +$path = $path_extra . PATH_SEPARATOR . $path; +ini_set('include_path', $path); + +$try_include = @include 'config.php'; + +if (!$try_include) { + header("Location: setup.php"); +} + +header('Cache-Control: no-cache'); +header('Pragma: no-cache'); + +if (function_exists('getOpenIDStore')) { + require_once 'lib/session.php'; + require_once 'lib/actions.php'; + + init(); + + $action = getAction(); + if (!function_exists($action)) { + $action = 'action_default'; + } + + $resp = $action(); + + writeResponse($resp); +} else { +?> +<html> + <head> + <title>PHP OpenID Server</title> + <body> + <h1>PHP OpenID Server</h1> + <p> + This server needs to be configured before it can be used. Edit + <code>config.php</code> to reflect your server's setup, then + load this page again. + </p> + </body> + </head> +</html> +<?php +} +?>
\ No newline at end of file diff --git a/examples/server/setup.php b/examples/server/setup.php new file mode 100644 index 0000000..e25ef34 --- /dev/null +++ b/examples/server/setup.php @@ -0,0 +1,558 @@ +<?php + +/** + * OpenID server configuration script. + * + * This script generates a config.php file needed by the server + * example. + * + * @package OpenID.Examples + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +$path_extra = dirname(dirname(dirname(__FILE__))); +$path = ini_get('include_path'); +$path = $path_extra . PATH_SEPARATOR . $path; +ini_set('include_path', $path); +require_once "Auth/OpenID.php"; + +/** + * Data. + */ + +$store_types = array("Filesystem" => "Auth_OpenID_FileStore", + "MySQL" => "Auth_OpenID_MySQLStore", + "PostgreSQL" => "Auth_OpenID_PostgreSQLStore", + "SQLite" => "Auth_OpenID_SQLiteStore"); + +/** + * Main. + */ + +$messages = array(); + +session_start(); +init_session(); + +if (!check_session() || + isset($_GET['add_openid'])) { + render_form(); +} else { + print generate_config(isset($_GET['download'])); +} + +/** + * Functions. + */ + +function check_url($url) { + return (Auth_OpenID::normalizeUrl($url) !== null); +} + +function build_url() { + $port = (($_SERVER['SERVER_PORT'] == 80) ? null : $_SERVER['SERVER_PORT']); + + $parts = explode("/", $_SERVER['SERVER_PROTOCOL']); + $scheme = strtolower($parts[0]); + + if ($port) { + return sprintf("%s://%s:%s%s/server.php", $scheme, $_SERVER['SERVER_NAME'], + $port, dirname($_SERVER['PHP_SELF'])); + } else { + return sprintf("%s://%s%s/server.php", $scheme, $_SERVER['SERVER_NAME'], + dirname($_SERVER['PHP_SELF'])); + } +} + +function check_open_basedir($path) { + if (ini_get('open_basedir')) { + $parts = explode(PATH_SEPARATOR, ini_get('open_basedir')); + + $found = false; + + foreach ($parts as $p) { + if (strpos($path, $p) === 0) { + $found = true; + break; + } + } + + return $found; + } else { + return true; + } +} + +function check_session() { + + global $messages; + + if ($_GET && isset($_GET['clear'])) { + session_destroy(); + $_SESSION = array(); + init_session(); + return false; + } + + $bad_path = false; + + if (isset($_GET['generate'])) { + if (!$_SESSION['server_url']) { + $messages[] = "Please enter a server URL."; + } + + if (!isset($_SESSION['store_type'])) { + $messages[] = "No store type chosen."; + } else { + switch ($_SESSION['store_type']) { + case "Filesystem": + if (!@$_SESSION['store_data']['fs_path']) { + $messages[] = "Please specify a filesystem store path."; + } else { + if (!check_open_basedir($_SESSION['store_data']['fs_path'])) { + $messages[] = "The filesystem store path violates PHP's <code>open_basedir</code> setting."; + $bad_path = true; + } + } + break; + + case "SQLite": + if (!@$_SESSION['store_data']['sqlite_path']) { + $messages[] = "Please specify a SQLite database path."; + } else { + if (!check_open_basedir($_SESSION['store_data']['sqlite_path'])) { + $messages[] = "The SQLite store path violates PHP's <code>open_basedir</code> setting."; + $bad_path = true; + } + } + break; + + default: + if (!($_SESSION['store_data']['host'] && + $_SESSION['store_data']['database'] && + $_SESSION['store_data']['username'] && + $_SESSION['store_data']['password'])) { + $messages[] = "Please specify database connection details."; + } + } + } + } + + if ($_SESSION['store_type'] && + $_SESSION['server_url'] && + (parse_url($_SESSION['server_url']) !== false) && + ((($_SESSION['store_type'] == 'Filesystem') && + $_SESSION['store_data']['fs_path']) || + (($_SESSION['store_type'] == 'SQLite') && + $_SESSION['store_data']['sqlite_path']) || + ($_SESSION['store_data']['host'] && + $_SESSION['store_data']['username'] && + $_SESSION['store_data']['database'] && + $_SESSION['store_data']['password'])) && + !$bad_path) { + + return true; + } + + return false; +} + +function render_form() { + + global $store_types, $fields, $messages; + + $basedir_msg = ""; + + if (ini_get('open_basedir')) { + $basedir_msg = "</br><span class=\"notice\">Note: Due to the ". + "<code>open_basedir</code> php.ini setting, be sure to ". + "choose a path in one of the following directories:<ul><li>". + implode("<li>", + explode(PATH_SEPARATOR, ini_get('open_basedir'))). + "</ul></span>"; + } + + $sqlite_found = false; + if (extension_loaded('sqlite') || + @dl('sqlite.' . PHP_SHLIB_SUFFIX)) { + $sqlite_found = true; + } + + $mysql_found = false; + if (extension_loaded('mysql') || + @dl('mysql.' . PHP_SHLIB_SUFFIX)) { + $mysql_found = true; + } + + $pgsql_found = false; + if (extension_loaded('pgsql') || + @dl('pgsql.' . PHP_SHLIB_SUFFIX)) { + $pgsql_found = true; + } + +?> +<html> + <head> + <style type="text/css"> +span.label { + float: left; + width: 2in; +} + +span.notice { + color: red; + font-size: 80%; +} + +div p { + border-top: 1px solid #ccc; + font-style: italic; + padding-top: 0.5em; +} + +div { + padding: 3px; +} + +div.store_fields { + margin-left: 2in; + padding: default; +} + +div.store_fields label.field { + float: left; + width: 1.75in; +} + +div.store_fields > div { + border: 1px solid gray; + margin-bottom: 0.5em; + background: #eee; +} + +div.store_fields > div > div { + margin-left: 0.4in; +} + +div.errors { + background: #faa; + border: 1px solid red; +} + +</style> +</head> +<body> + +<h2>OpenID Example Server Configuration</h2> + +<?php +if ($messages) { + print "<div class=\"errors\">"; + foreach ($messages as $m) { + print "<div>$m</div>"; + } + print "</div>"; + +} +?> + +<p> +Your browser has been redirected to this page so you can configure the +server example. This form will auto-generate an OpenID example server +configuration for use with the OpenID server example. +</p> + +<form> +<div> + + <p> + The server URL is the URL that points to the "server.php" file. It + looks like your server URL should be <code><?php print build_url(); ?></code>. + </p> + + <span class="label"><label for="i_server_url">Server URL:</label></span> + <span> + <input type="text" id="i_server_url" size="35" name="server_url" + value="<?php print $_SESSION['server_url'] ?>"> + </span> +</div> + +<div> + + <p> + If this package isn't installed in the PHP include path, the package's + directory should be added. For example, if the package is in + <code>/home/me/PHP-OpenID/</code>, you should enter that directory here. + </p> + + <span class="label"> + <label for="i_include_path">Include path (optional):</label> + </span> + <span> + <input type="text" id="i_include_path" size="35" name="include_path" + value="<?php print $_SESSION['include_path'] ?>"> + </span> +</div> + +<div> + + <p> + The server needs to store OpenID information in a "store". The + following store types are available on your PHP installation: + </p> + + <span class="label">Store method:</span> + <div class="store_fields"> + + <div> + <input type="radio" name="store_type" value="Filesystem" + id="i_filesystem"<?php if ($_SESSION['store_type'] == 'Filesystem') { print " CHECKED"; } ?>> + <label for="i_filesystem">Filesystem</label> + <div> + <label for="i_fs_path" class="field">Filesystem path:</label> + <input type="text" name="fs_path" id="i_fs_path" + value="<?php print @$_SESSION['store_data']['fs_path']; ?>"> + <?php print $basedir_msg; ?> + </div> + </div> + +<?php if ($sqlite_found) { ?> + <div> + <input type="radio" name="store_type" value="SQLite" + id="i_sqlite"<?php if ($_SESSION['store_type'] == 'SQLite') { print " CHECKED"; } ?>> + <label for="i_sqlite">SQLite</label> + <div> + <label for="i_sqlite_path" class="field">SQLite database path:</label> + <input type="text" value="<?php print @$_SESSION['store_data']['sqlite_path']; ?>" + name="sqlite_path" id="i_sqlite_path"> + <?php print $basedir_msg; ?> + </div> + </div> +<?php } ?> + + +<?php if ($mysql_found || $pgsql_found) { ?> + <div> + +<?php if ($mysql_found) { ?> + <input type="radio" name="store_type" value="MySQL" + id="i_mysql"<?php if ($_SESSION['store_type'] == 'MySQL') { print " CHECKED"; } ?>> + <label for="i_mysql">MySQL</label> +<?php } ?> + +<?php if ($pgsql_found) { ?> + <input type="radio" name="store_type" value="PostgreSQL" + id="i_pgsql"<?php if ($_SESSION['store_type'] == 'PostgreSQL') { print " CHECKED"; } ?>> + <label for="i_pgsql">PostgreSQL</label> +<?php } ?> + + <div> + <label for="i_m_host" class="field">Host:</label> + <input type="text" value="<?php print @$_SESSION['store_data']['host']; ?>" name="host" id="i_m_host"> + </div> + <div> + <label for="i_m_database" class="field">Database:</label> + <input value="<?php print @$_SESSION['store_data']['database']; ?>" type="text" name="database" id="i_m_database"> + </div> + <div> + <label for="i_m_username" class="field">Username:</label> + <input type="text" name="username" id="i_m_username" value="<?php print @$_SESSION['store_data']['username']; ?>"> + </div> + <div> + <label for="i_m_password" class="field">Password:</label> + <input type="password" name="password" id="i_m_password" value="<?php print @$_SESSION['store_data']['password']; ?>"> + </div> + </div> +<?php } ?> +</div> +</div> + +<input type="submit" name="generate" value="Generate Configuration"> +</form> +</body> +</html> +<?php +} + +function init_session() { + + global $messages; + + // Set a guess value for the server url. + if (!array_key_exists('server_url', $_SESSION)) { + $_SESSION['server_url'] = build_url(); + } + + foreach (array('server_url', 'include_path', 'store_type') as $key) { + if (!isset($_SESSION[$key])) { + $_SESSION[$key] = ""; + } + } + + if (!isset($_SESSION['store_data'])) { + $_SESSION['store_data'] = array(); + } + + foreach (array('server_url', 'include_path', 'store_type') as $field) { + if (array_key_exists($field, $_GET)) { + $_SESSION[$field] = $_GET[$field]; + } + } + + foreach (array('username', 'password', 'database', 'host', 'fs_path', 'sqlite_path') as $field) { + if (array_key_exists($field, $_GET)) { + $_SESSION['store_data'][$field] = $_GET[$field]; + } + } +} + +function generate_config($download = false) { + + if ($download) { + // Emit headers to force browser download. + header("Content-type: text/plain"); + header("Content-disposition: attachment; filename=config.php"); + print "<?php\n"; + } else { +?> +<html> +<body> + +<h2>OpenID Example Server Configuration</h2> + +<p> +Put the following text into <strong><?php print dirname(__FILE__); print DIRECTORY_SEPARATOR; ?>config.php</strong>. +</p> + +<p> +<a href="setup.php?clear=1">Back to form</a> (resets settings) +</p> + +<p> +<a href="setup.php?download=1">Download this configuration</a> +</p> + +<pre style="border: 1px solid gray; background: #eee; padding: 5px;"> +<?php +print "<?php\n"; +} +?> +<?php if ($_SESSION['include_path']) { ?> +/** + * Set any extra include paths needed to use the library + */ +set_include_path(get_include_path() . PATH_SEPARATOR . "<?php +print $_SESSION['include_path']; +?>"); + +<?php } ?> +/** + * The URL for the server. + * + * This is the location of server.php. For example: + * + * $server_url = 'http://example.com/~user/server.php'; + * + * This must be a full URL. + */ +$server_url = "<?php +print $_SESSION['server_url']; +?>"; + +/** + * Initialize an OpenID store + * + * @return object $store an instance of OpenID store (see the + * documentation for how to create one) + */ +function getOpenIDStore() +{ + <?php + + switch ($_SESSION['store_type']) { + case "Filesystem": + + print "require_once \"Auth/OpenID/FileStore.php\";\n "; + print "return new Auth_OpenID_FileStore(\"".$_SESSION['store_data']['fs_path']."\");\n"; + break; + + case "SQLite": + + print "require_once \"Auth/OpenID/SQLiteStore.php\";\n "; + print "\$s = new Auth_OpenID_SQLiteStore(\"".$_SESSION['store_data']['sqlite_path']."\");\n "; + print "\$s->createTables();\n "; + print "return \$s;\n"; + break; + + case "MySQL": + + ?>require_once 'Auth/OpenID/MySQLStore.php'; + require_once 'DB.php'; + + $dsn = array( + 'phptype' => 'mysql', + 'username' => '<?php print $_SESSION['store_data']['username']; ?>', + 'password' => '<?php print $_SESSION['store_data']['password']; ?>', + 'hostspec' => '<?php print $_SESSION['store_data']['host']; ?>' + ); + + $db =& DB::connect($dsn); + + if (PEAR::isError($db)) { + return null; + } + + $db->query("USE <?php print $_SESSION['store_data']['database']; ?>"); + + $s =& new Auth_OpenID_MySQLStore($db); + + $s->createTables(); + + return $s; +<?php + break; + + case "PostgreSQL": + + ?>require_once 'Auth/OpenID/PostgreSQLStore.php'; + require_once 'DB.php'; + + $dsn = array( + 'phptype' => 'pgsql', + 'username' => '<?php print $_SESSION['store_data']['username']; ?>', + 'password' => '<?php print $_SESSION['store_data']['password']; ?>', + 'hostspec' => '<?php print $_SESSION['store_data']['host']; ?>', + 'database' => '<?php print $_SESSION['store_data']['database']; ?>' + ); + + $db =& DB::connect($dsn); + + if (PEAR::isError($db)) { + return null; + } + + $s =& new Auth_OpenID_PostgreSQLStore($db); + + $s->createTables(); + + return $s; +<?php + break; + } + + ?> +} + +<?php + print "?>"; + if (!$download) { +?> +</pre> +</body> +</html> +<?php + } + } // end function generate_config () +?> |