From 85551fe3e619df8344c20e1a6a73896b5312e88a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=85kre=20Solberg?= <andreas.solberg@uninett.no> Date: Thu, 8 Nov 2007 09:48:30 +0000 Subject: [PATCH] Adding OpenID libraries, templates and server endpoint git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@68 44740490-163a-0410-bde0-09ae8108e29a --- lib/Auth/OpenID.php | 412 ++++++ lib/Auth/OpenID/Association.php | 308 +++++ lib/Auth/OpenID/BigMath.php | 444 +++++++ lib/Auth/OpenID/Consumer.php | 1186 ++++++++++++++++++ lib/Auth/OpenID/CryptUtil.php | 109 ++ lib/Auth/OpenID/DatabaseConnection.php | 131 ++ lib/Auth/OpenID/DiffieHellman.php | 181 +++ lib/Auth/OpenID/Discover.php | 258 ++++ lib/Auth/OpenID/DumbStore.php | 116 ++ lib/Auth/OpenID/FileStore.php | 674 ++++++++++ lib/Auth/OpenID/HMACSHA1.php | 72 ++ lib/Auth/OpenID/Interface.php | 188 +++ lib/Auth/OpenID/KVForm.php | 112 ++ lib/Auth/OpenID/MySQLStore.php | 78 ++ lib/Auth/OpenID/Parse.php | 308 +++++ lib/Auth/OpenID/PostgreSQLStore.php | 136 ++ lib/Auth/OpenID/SQLStore.php | 658 ++++++++++ lib/Auth/OpenID/SQLiteStore.php | 66 + lib/Auth/OpenID/Server.php | 1307 ++++++++++++++++++++ lib/Auth/OpenID/ServerRequest.php | 37 + lib/Auth/OpenID/TrustRoot.php | 243 ++++ lib/Auth/OpenID/URINorm.php | 231 ++++ lib/Services/Yadis/HTTPFetcher.php | 92 ++ lib/Services/Yadis/Manager.php | 496 ++++++++ lib/Services/Yadis/Misc.php | 59 + lib/Services/Yadis/ParanoidHTTPFetcher.php | 177 +++ lib/Services/Yadis/ParseHTML.php | 258 ++++ lib/Services/Yadis/PlainHTTPFetcher.php | 245 ++++ lib/Services/Yadis/XML.php | 365 ++++++ lib/Services/Yadis/XRDS.php | 425 +++++++ lib/Services/Yadis/XRI.php | 233 ++++ lib/Services/Yadis/XRIRes.php | 68 + lib/Services/Yadis/Yadis.php | 313 +++++ templates/default/en/openid-about.php | 60 + templates/default/en/openid-sites.php | 81 ++ templates/default/en/openid-trust.php | 33 + www/openid/provider/server.php | 676 ++++++++++ 37 files changed, 10836 insertions(+) create mode 100644 lib/Auth/OpenID.php create mode 100644 lib/Auth/OpenID/Association.php create mode 100644 lib/Auth/OpenID/BigMath.php create mode 100644 lib/Auth/OpenID/Consumer.php create mode 100644 lib/Auth/OpenID/CryptUtil.php create mode 100644 lib/Auth/OpenID/DatabaseConnection.php create mode 100644 lib/Auth/OpenID/DiffieHellman.php create mode 100644 lib/Auth/OpenID/Discover.php create mode 100644 lib/Auth/OpenID/DumbStore.php create mode 100644 lib/Auth/OpenID/FileStore.php create mode 100644 lib/Auth/OpenID/HMACSHA1.php create mode 100644 lib/Auth/OpenID/Interface.php create mode 100644 lib/Auth/OpenID/KVForm.php create mode 100644 lib/Auth/OpenID/MySQLStore.php create mode 100644 lib/Auth/OpenID/Parse.php create mode 100644 lib/Auth/OpenID/PostgreSQLStore.php create mode 100644 lib/Auth/OpenID/SQLStore.php create mode 100644 lib/Auth/OpenID/SQLiteStore.php create mode 100644 lib/Auth/OpenID/Server.php create mode 100644 lib/Auth/OpenID/ServerRequest.php create mode 100644 lib/Auth/OpenID/TrustRoot.php create mode 100644 lib/Auth/OpenID/URINorm.php create mode 100644 lib/Services/Yadis/HTTPFetcher.php create mode 100644 lib/Services/Yadis/Manager.php create mode 100644 lib/Services/Yadis/Misc.php create mode 100644 lib/Services/Yadis/ParanoidHTTPFetcher.php create mode 100644 lib/Services/Yadis/ParseHTML.php create mode 100644 lib/Services/Yadis/PlainHTTPFetcher.php create mode 100644 lib/Services/Yadis/XML.php create mode 100644 lib/Services/Yadis/XRDS.php create mode 100644 lib/Services/Yadis/XRI.php create mode 100644 lib/Services/Yadis/XRIRes.php create mode 100644 lib/Services/Yadis/Yadis.php create mode 100644 templates/default/en/openid-about.php create mode 100644 templates/default/en/openid-sites.php create mode 100644 templates/default/en/openid-trust.php create mode 100644 www/openid/provider/server.php diff --git a/lib/Auth/OpenID.php b/lib/Auth/OpenID.php new file mode 100644 index 000000000..86278b9f1 --- /dev/null +++ b/lib/Auth/OpenID.php @@ -0,0 +1,412 @@ +<?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 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * Require the fetcher code. + */ +require_once "Services/Yadis/PlainHTTPFetcher.php"; +require_once "Services/Yadis/ParanoidHTTPFetcher.php"; +require_once "Auth/OpenID/BigMath.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) { + define('Auth_OpenID_NO_MATH_SUPPORT', true); +} + +/** + * The OpenID utility function class. + * + * @package OpenID + * @access private + */ +class Auth_OpenID { + + /** + * These namespaces are automatically fixed in query arguments by + * Auth_OpenID::fixArgs. + */ + function getOpenIDNamespaces() + { + return array('openid', + 'sreg'); + } + + /** + * Rename query arguments back to 'openid.' from 'openid_' + * + * @access private + * @param array $args An associative array of URL query arguments + */ + function fixArgs($args) + { + foreach (array_keys($args) as $key) { + $fixed = $key; + if (preg_match('/^openid/', $key)) { + foreach (Auth_OpenID::getOpenIDNamespaces() as $ns) { + if (preg_match('/'.$ns.'_/', $key)) { + $fixed = preg_replace('/'.$ns.'_/', $ns.'.', $fixed); + } + } + + if ($fixed != $key) { + $val = $args[$key]; + unset($args[$key]); + $args[$fixed] = $val; + } + } + } + + return $args; + } + + /** + * 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 { + if (Auth_OpenID::ensureDir(dirname($dir_name))) { + return is_dir($dir_name) || @mkdir($dir_name); + } else { + return false; + } + } + } + + /** + * Convenience function for getting array values. + * + * @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 expected " . + "array as first parameter", E_USER_WARNING); + return false; + } + } + + /** + * 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). + * + * @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); + } + + /** + * Turn a string into an ASCII string. + * + * Replace non-ascii characters with a %-encoded, UTF-8 + * encoding. This function will fail if the input is a string and + * there are non-7-bit-safe characters. It is assumed that the + * caller will have already translated the input into a Unicode + * character sequence, according to the encoding of the HTTP POST + * or GET. + * + * Do not escape anything that is already 7-bit safe, so we do the + * minimal transform on the identity URL + * + * @access private + */ + function quoteMinimal($s) + { + $res = array(); + for ($i = 0; $i < strlen($s); $i++) { + $c = $s[$i]; + if ($c >= "\x80") { + for ($j = 0; $j < count(utf8_encode($c)); $j++) { + array_push($res, sprintf("%02X", ord($c[$j]))); + } + } else { + array_push($res, $c); + } + } + + return implode('', $res); + } + + /** + * Implements python's urlunparse, which is not available in PHP. + * Given the specified components of a URL, this function rebuilds + * 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) + { + if ($url === null) { + return null; + } + + assert(is_string($url)); + + $old_url = $url; + $url = trim($url); + + if (strpos($url, "://") === false) { + $url = "http://" . $url; + } + + $parsed = @parse_url($url); + + if ($parsed === false) { + return null; + } + + $defaults = array( + 'scheme' => '', + 'host' => '', + 'path' => '', + 'query' => '', + 'fragment' => '', + 'port' => '' + ); + + $parsed = array_merge($defaults, $parsed); + + if (($parsed['scheme'] == '') || + ($parsed['host'] == '')) { + if ($parsed['path'] == '' && + $parsed['query'] == '' && + $parsed['fragment'] == '') { + return null; + } + + $url = 'http://' + $url; + $parsed = parse_url($url); + + $parsed = array_merge($defaults, $parsed); + } + + $tail = array_map(array('Auth_OpenID', 'quoteMinimal'), + array($parsed['path'], + $parsed['query'], + $parsed['fragment'])); + if ($tail[0] == '') { + $tail[0] = '/'; + } + + $url = Auth_OpenID::urlunparse($parsed['scheme'], $parsed['host'], + $parsed['port'], $tail[0], $tail[1], + $tail[2]); + + assert(is_string($url)); + + return $url; + } +} + +?> \ No newline at end of file diff --git a/lib/Auth/OpenID/Association.php b/lib/Auth/OpenID/Association.php new file mode 100644 index 000000000..109a97080 --- /dev/null +++ b/lib/Auth/OpenID/Association.php @@ -0,0 +1,308 @@ +<?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 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * @access private + */ +require_once 'Auth/OpenID/CryptUtil.php'; + +/** + * @access private + */ +require_once 'Auth/OpenID/KVForm.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' + ); + + /** + * 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 value of this field at + * this time is 'HMAC-SHA1', 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 value of this field at + * this time is 'HMAC-SHA1', but new types may be defined in the + * future. + */ + function Auth_OpenID_Association( + $handle, $secret, $issued, $lifetime, $assoc_type) + { + if ($assoc_type != 'HMAC-SHA1') { + $fmt = 'HMAC-SHA1 is the only supported association type (got %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: ' . strval($keys), + 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); + return Auth_OpenID_HMACSHA1($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 signDict($fields, $data, $prefix = 'openid.') + { + $pairs = array(); + foreach ($fields as $field) { + $pairs[] = array($field, $data[$prefix . $field]); + } + + return base64_encode($this->sign($pairs)); + } + + /** + * Add a signature to an array of fields + * + * @access private + */ + function addSignature($fields, &$data, $prefix = 'openid.') + { + $sig = $this->signDict($fields, $data, $prefix); + $signed = implode(",", $fields); + $data[$prefix . 'sig'] = $sig; + $data[$prefix . 'signed'] = $signed; + } + + /** + * Confirm that the signature of these fields matches the + * signature contained in the data + * + * @access private + */ + function checkSignature($data, $prefix = 'openid.') + { + $signed = $data[$prefix . 'signed']; + $fields = explode(",", $signed); + $expected_sig = $this->signDict($fields, $data, $prefix); + $request_sig = $data[$prefix . 'sig']; + + return ($request_sig == $expected_sig); + } +} + +?> \ No newline at end of file diff --git a/lib/Auth/OpenID/BigMath.php b/lib/Auth/OpenID/BigMath.php new file mode 100644 index 000000000..3113104ba --- /dev/null +++ b/lib/Auth/OpenID/BigMath.php @@ -0,0 +1,444 @@ +<?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 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * Needed for random number generation + */ +require_once 'Auth/OpenID/CryptUtil.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 = strlen($rbytes) - 1; + } else { + $nbytes = strlen($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. + */ +global $_Auth_OpenID_math_extensions; +$_Auth_OpenID_math_extensions = array( + array('modules' => array('gmp', 'php_gmp'), + 'extension' => 'gmp', + 'class' => 'Auth_OpenID_GmpMathWrapper'), + array('modules' => array('bcmath', 'php_bcmath'), + 'extension' => 'bcmath', + 'class' => 'Auth_OpenID_BcMathWrapper') + ); + +/** + * 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 + * local {@link Auth_OpenID_math_extensions} array 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 (defined('Auth_OpenID_NO_MATH_SUPPORT')) { + $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. + global $_Auth_OpenID_math_extensions; + $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); + + define('Auth_OpenID_NO_MATH_SUPPORT', true); + return null; + } + + // Instantiate a new wrapper + $class = $ext['class']; + $lib = new $class(); + + return $lib; +} + +?> \ No newline at end of file diff --git a/lib/Auth/OpenID/Consumer.php b/lib/Auth/OpenID/Consumer.php new file mode 100644 index 000000000..7ea75c75a --- /dev/null +++ b/lib/Auth/OpenID/Consumer.php @@ -0,0 +1,1186 @@ +<?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. In addition to saying that, the identity server + * provides a URL to which the user can be sent to provide the needed + * information and let the server finish handling the original + * request. + * + * 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 any sort of session + * framework that provides per-client state management, a dict-like + * object to access the session should be passed as the optional + * second parameter. (The default behavior is to use PHP's standard + * session machinery.) + * + * 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 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * Require utility classes and functions for the consumer. + */ +require_once "Auth/OpenID.php"; +require_once "Auth/OpenID/HMACSHA1.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/Discover.php"; +require_once "Services/Yadis/Manager.php"; +require_once "Services/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'); + +/** + * This is the characters that the nonces are made from. + */ +define('Auth_OpenID_DEFAULT_NONCE_CHRS',"abcdefghijklmnopqrstuvwxyz" . + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); + +/** + * 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 $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 Services_Yadis_Session 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. + */ + function Auth_OpenID_Consumer(&$store, $session = null) + { + if ($session === null) { + $session = new Services_Yadis_PHPSession(); + } + + $this->session =& $session; + $this->consumer =& new Auth_OpenID_GenericConsumer($store); + $this->_token_key = $this->session_key_prefix . $this->_token_suffix; + } + + /** + * Start the OpenID authentication process. See steps 1-2 in the + * overview at the top of this file. + * + * @param 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. + * + * @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) + { + $discoverMethod = '_Auth_OpenID_discoverServiceList'; + $openid_url = $user_url; + + if (Services_Yadis_identifierScheme($user_url) == 'XRI') { + $discoverMethod = '_Auth_OpenID_discoverXRIServiceList'; + } else { + $openid_url = Auth_OpenID::normalizeUrl($user_url); + } + + $disco =& new Services_Yadis_Discovery($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 Services_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($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); + } + } + + /** + * 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. + * + * @return Auth_OpenID_AuthRequest $auth_request An OpenID + * authentication request object. + */ + function &beginWithoutDiscovery($endpoint) + { + $loader = new Auth_OpenID_ServiceEndpointLoader(); + $auth_req = $this->consumer->begin($endpoint); + $this->session->set($this->_token_key, + $loader->toSession($auth_req->endpoint)); + 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 array $query An array of the query parameters (key => + * value pairs) for this HTTP request. + * + * @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($query) + { + $query = Auth_OpenID::fixArgs($query); + + $loader = new Auth_OpenID_ServiceEndpointLoader(); + $endpoint_data = $this->session->get($this->_token_key); + $endpoint = + $loader->fromSession($endpoint_data); + + if ($endpoint === null) { + $response = new Auth_OpenID_FailureResponse(null, + 'No session state found'); + } else { + $response = $this->consumer->complete($query, $endpoint); + $this->session->del($this->_token_key); + } + + if (in_array($response->status, array(Auth_OpenID_SUCCESS, + Auth_OpenID_CANCEL))) { + if ($response->identity_url !== null) { + $disco = new Services_Yadis_Discovery($this->session, + $response->identity_url, + $this->session_key_prefix); + $disco->cleanup(); + } + } + + return $response; + } +} + +class Auth_OpenID_DiffieHellmanConsumerSession { + var $session_type = 'DH-SHA1'; + + function Auth_OpenID_DiffieHellmanConsumerSession($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('openid.dh_consumer_public' => $cpub); + + if (!$this->dh->usingDefaultValues()) { + $args = array_merge($args, array( + 'openid.dh_modulus' => + $math->longToBase64($this->dh->mod), + 'openid.dh_gen' => + $math->longToBase64($this->dh->gen))); + } + + return $args; + } + + function extractSecret($response) + { + if (!array_key_exists('dh_server_public', $response)) { + return null; + } + + if (!array_key_exists('enc_mac_key', $response)) { + return null; + } + + $math =& Auth_OpenID_getMathLib(); + $spub = $math->base64ToLong($response['dh_server_public']); + $enc_mac_key = base64_decode($response['enc_mac_key']); + + return $this->dh->xorSecret($spub, $enc_mac_key); + } +} + +class Auth_OpenID_PlainTextConsumerSession { + var $session_type = null; + + function getRequest() + { + return array(); + } + + function extractSecret($response) + { + if (!array_key_exists('mac_key', $response)) { + return null; + } + + return base64_decode($response['mac_key']); + } +} + +/** + * 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 + * @access private + */ +class Auth_OpenID_GenericConsumer { + /** + * This consumer's store object. + */ + var $store; + + /** + * @access private + */ + var $_use_assocs; + + /** + * This is the number of characters in the generated nonce for + * each transaction. + */ + var $nonce_len = 8; + + /** + * What characters are allowed in nonces + */ + var $nonce_chrs = Auth_OpenID_DEFAULT_NONCE_CHRS; + + /** + * 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->_use_assocs = + !(defined('Auth_OpenID_NO_MATH_SUPPORT') || + ($this->store && $this->store->isDumb())); + + $this->fetcher = Services_Yadis_Yadis::getHTTPFetcher(); + } + + function begin($service_endpoint) + { + $nonce = $this->_createNonce(); + $assoc = $this->_getAssociation($service_endpoint->server_url); + $r = new Auth_OpenID_AuthRequest($assoc, $service_endpoint); + $r->return_to_args['nonce'] = $nonce; + return $r; + } + + function complete($query, $endpoint) + { + $mode = Auth_OpenID::arrayGet($query, 'openid.mode', + '<no mode specified>'); + + if ($mode == Auth_OpenID_CANCEL) { + return new Auth_OpenID_CancelResponse($endpoint); + } else if ($mode == 'error') { + $error = Auth_OpenID::arrayGet($query, 'openid.error'); + return new Auth_OpenID_FailureResponse($endpoint, $error); + } else if ($mode == 'id_res') { + if ($endpoint->identity_url === null) { + return new Auth_OpenID_FailureResponse($identity_url, + "No session state found"); + } + + $response = $this->_doIdRes($query, $endpoint); + + if ($response === null) { + return new Auth_OpenID_FailureResponse($endpoint, + "HTTP request failed"); + } + if ($response->status == Auth_OpenID_SUCCESS) { + return $this->_checkNonce($response, + Auth_OpenID::arrayGet($query, + 'nonce')); + } else { + return $response; + } + } else { + return new Auth_OpenID_FailureResponse($endpoint, + sprintf("Invalid openid.mode '%s'", + $mode)); + } + } + + /** + * @access private + */ + function _doIdRes($query, $endpoint) + { + $user_setup_url = Auth_OpenID::arrayGet($query, + 'openid.user_setup_url'); + + if ($user_setup_url !== null) { + return new Auth_OpenID_SetupNeededResponse($endpoint, + $user_setup_url); + } + + $return_to = Auth_OpenID::arrayGet($query, 'openid.return_to', null); + $server_id2 = Auth_OpenID::arrayGet($query, 'openid.identity', null); + $assoc_handle = Auth_OpenID::arrayGet($query, + 'openid.assoc_handle', null); + + if (($return_to === null) || + ($server_id2 === null) || + ($assoc_handle === null)) { + return new Auth_OpenID_FailureResponse($endpoint, + "Missing required field"); + } + + if ($endpoint->getServerID() != $server_id2) { + return new Auth_OpenID_FailureResponse($endpoint, + "Server ID (delegate) mismatch"); + } + + $signed = Auth_OpenID::arrayGet($query, 'openid.signed'); + + $assoc = $this->store->getAssociation($endpoint->server_url, + $assoc_handle); + + if ($assoc === null) { + // It's not an association we know about. Dumb mode is + // our only possible path for recovery. + if ($this->_checkAuth($query, $endpoint->server_url)) { + return new Auth_OpenID_SuccessResponse($endpoint, $query, + $signed); + } else { + return new Auth_OpenID_FailureResponse($endpoint, + "Server denied check_authentication"); + } + } + + if ($assoc->getExpiresIn() <= 0) { + $msg = sprintf("Association with %s expired", + $endpoint->server_url); + return new Auth_OpenID_FailureResponse($endpoint, $msg); + } + + // Check the signature + $sig = Auth_OpenID::arrayGet($query, 'openid.sig', null); + if (($sig === null) || + ($signed === null)) { + return new Auth_OpenID_FailureResponse($endpoint, + "Missing argument signature"); + } + + $signed_list = explode(",", $signed); + + //Fail if the identity field is present but not signed + if (($endpoint->identity_url !== null) && + (!in_array('identity', $signed_list))) { + $msg = '"openid.identity" not signed'; + return new Auth_OpenID_FailureResponse($endpoint, $msg); + } + + $v_sig = $assoc->signDict($signed_list, $query); + + if ($v_sig != $sig) { + return new Auth_OpenID_FailureResponse($endpoint, + "Bad signature"); + } + + return Auth_OpenID_SuccessResponse::fromQuery($endpoint, + $query, $signed); + } + + /** + * @access private + */ + function _checkAuth($query, $server_url) + { + $request = $this->_createCheckAuthRequest($query); + if ($request === null) { + return false; + } + + $response = $this->_makeKVPost($request, $server_url); + if ($response == null) { + return false; + } + + return $this->_processCheckAuthResponse($response, $server_url); + } + + /** + * @access private + */ + function _createCheckAuthRequest($query) + { + $signed = Auth_OpenID::arrayGet($query, 'openid.signed', null); + if ($signed === null) { + return null; + } + + $whitelist = array('assoc_handle', 'sig', + 'signed', 'invalidate_handle'); + + $signed = array_merge(explode(",", $signed), $whitelist); + + $check_args = array(); + + foreach ($query as $key => $value) { + if (in_array(substr($key, 7), $signed)) { + $check_args[$key] = $value; + } + } + + $check_args['openid.mode'] = 'check_authentication'; + return $check_args; + } + + /** + * @access private + */ + function _processCheckAuthResponse($response, $server_url) + { + $is_valid = Auth_OpenID::arrayGet($response, 'is_valid', 'false'); + + $invalidate_handle = Auth_OpenID::arrayGet($response, + 'invalidate_handle'); + + if ($invalidate_handle !== null) { + $this->store->removeAssociation($server_url, + $invalidate_handle); + } + + if ($is_valid == 'true') { + return true; + } + + return false; + } + + /** + * @access private + */ + function _makeKVPost($args, $server_url) + { + $mode = $args['openid.mode']; + + $pairs = array(); + foreach ($args as $k => $v) { + $v = urlencode($v); + $pairs[] = "$k=$v"; + } + + $body = implode("&", $pairs); + + $resp = $this->fetcher->post($server_url, $body); + + if ($resp === null) { + return null; + } + + $response = Auth_OpenID_KVForm::toArray($resp->body); + + if ($resp->status == 400) { + return null; + } else if ($resp->status != 200) { + return null; + } + + return $response; + } + + /** + * @access private + */ + function _checkNonce($response, $nonce) + { + $parsed_url = parse_url($response->getReturnTo()); + $query_str = @$parsed_url['query']; + $query = array(); + parse_str($query_str, $query); + + $found = false; + + foreach ($query as $k => $v) { + if ($k == 'nonce') { + if ($v != $nonce) { + return new Auth_OpenID_FailureResponse($response, + "Nonce mismatch"); + } else { + $found = true; + break; + } + } + } + + if (!$found) { + return new Auth_OpenID_FailureResponse($response, + sprintf("Nonce missing from return_to: %s", + $response->getReturnTo())); + } + + if (!$this->store->useNonce($nonce)) { + return new Auth_OpenID_FailureResponse($response, + "Nonce missing from store"); + } + + return $response; + } + + /** + * @access private + */ + function _createNonce() + { + $nonce = Auth_OpenID_CryptUtil::randomString($this->nonce_len, + $this->nonce_chrs); + $this->store->storeNonce($nonce); + return $nonce; + } + + /** + * @access protected + */ + function _createDiffieHellman() + { + return new Auth_OpenID_DiffieHellman(); + } + + /** + * @access private + */ + function _getAssociation($server_url) + { + if (!$this->_use_assocs) { + return null; + } + + $assoc = $this->store->getAssociation($server_url); + + if (($assoc === null) || + ($assoc->getExpiresIn() <= 0)) { + + $parts = $this->_createAssociateRequest($server_url); + + if ($parts === null) { + return null; + } + + list($assoc_session, $args) = $parts; + + $response = $this->_makeKVPost($args, $server_url); + + if ($response === null) { + $assoc = null; + } else { + $assoc = $this->_parseAssociation($response, $assoc_session, + $server_url); + } + } + + return $assoc; + } + + function _createAssociateRequest($server_url) + { + $parts = parse_url($server_url); + + if ($parts === false) { + return null; + } + + if (array_key_exists('scheme', $parts)) { + $proto = $parts['scheme']; + } else { + $proto = 'http'; + } + + if ($proto == 'https') { + $assoc_session = new Auth_OpenID_PlainTextConsumerSession(); + } else { + $assoc_session = new Auth_OpenID_DiffieHellmanConsumerSession(); + } + + $args = array( + 'openid.mode' => 'associate', + 'openid.assoc_type' => 'HMAC-SHA1'); + + if ($assoc_session->session_type !== null) { + $args['openid.session_type'] = $assoc_session->session_type; + } + + $args = array_merge($args, $assoc_session->getRequest()); + return array($assoc_session, $args); + } + + /** + * @access private + */ + function _parseAssociation($results, $assoc_session, $server_url) + { + $required_keys = array('assoc_type', 'assoc_handle', + 'expires_in'); + + foreach ($required_keys as $key) { + if (!array_key_exists($key, $results)) { + return null; + } + } + + $assoc_type = $results['assoc_type']; + $assoc_handle = $results['assoc_handle']; + $expires_in_str = $results['expires_in']; + + if ($assoc_type != 'HMAC-SHA1') { + return null; + } + + $expires_in = intval($expires_in_str); + + if ($expires_in <= 0) { + return null; + } + + $session_type = Auth_OpenID::arrayGet($results, 'session_type'); + if ($session_type != $assoc_session->session_type) { + if ($session_type === null) { + $assoc_session = new Auth_OpenID_PlainTextConsumerSession(); + } else { + return null; + } + } + + $secret = $assoc_session->extractSecret($results); + + if (!$secret) { + return null; + } + + $assoc = Auth_OpenID_Association::fromExpiresIn( + $expires_in, $assoc_handle, $secret, $assoc_type); + $this->store->storeAssociation($server_url, $assoc); + + return $assoc; + } +} + +/** + * 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($assoc, $endpoint) + { + $this->assoc = $assoc; + $this->endpoint = $endpoint; + $this->extra_args = array(); + $this->return_to_args = array(); + } + + /** + * 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) + { + $arg_name = implode('.', array('openid', $namespace, $key)); + $this->extra_args[$arg_name] = $value; + } + + /** + * Compute the appropriate redirection URL for this request based + * on a specified trust root and return-to. + * + * @param string $trust_root The trust root URI for your + * application. + * + * @param string$ $return_to The return-to URL to be used when the + * OpenID server redirects the user back to your site. + * + * @return string $redirect_url The resulting redirect URL that + * you should send to the user agent. + */ + function redirectURL($trust_root, $return_to, $immediate=false) + { + if ($immediate) { + $mode = 'checkid_immediate'; + } else { + $mode = 'checkid_setup'; + } + + $return_to = Auth_OpenID::appendArgs($return_to, $this->return_to_args); + + $redir_args = array( + 'openid.mode' => $mode, + 'openid.identity' => $this->endpoint->getServerID(), + 'openid.return_to' => $return_to, + 'openid.trust_root' => $trust_root); + + if ($this->assoc) { + $redir_args['openid.assoc_handle'] = $this->assoc->handle; + } + + $redir_args = array_merge($redir_args, $this->extra_args); + + return Auth_OpenID::appendArgs($this->endpoint->server_url, + $redir_args); + } +} + +/** + * The base class for responses from the Auth_OpenID_Consumer. + * + * @package OpenID + */ +class Auth_OpenID_ConsumerResponse { + var $status = 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: + * + * identity_url - 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, $signed_args) + { + $this->endpoint = $endpoint; + $this->identity_url = $endpoint->identity_url; + $this->signed_args = $signed_args; + } + + /** + * @access private + */ + function fromQuery($endpoint, $query, $signed) + { + $signed_args = array(); + foreach (explode(",", $signed) as $field_name) { + $field_name = 'openid.' . $field_name; + $signed_args[$field_name] = Auth_OpenID::arrayGet($query, + $field_name, ''); + } + return new Auth_OpenID_SuccessResponse($endpoint, $signed_args); + } + + /** + * Extract signed extension data from the server's response. + * + * @param string $prefix The extension namespace from which to + * extract the extension data. + */ + function extensionResponse($prefix) + { + $response = array(); + $prefix = sprintf('openid.%s.', $prefix); + $prefix_len = strlen($prefix); + foreach ($this->signed_args as $k => $v) { + if (strpos($k, $prefix) === 0) { + $response_key = substr($k, $prefix_len); + $response[$response_key] = $v; + } + } + + return $response; + } + + /** + * 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 Auth_OpenID::arrayGet($this->signed_args, 'openid.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: + * + * identity_url - 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) + { + $this->endpoint = $endpoint; + if ($endpoint !== null) { + $this->identity_url = $endpoint->identity_url; + } else { + $this->identity_url = null; + } + $this->message = $message; + } +} + +/** + * A response with a status of Auth_OpenID_CANCEL. Indicates that the + * user cancelled the OpenID authentication request. This has two + * relevant attributes: + * + * identity_url - 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->endpoint = $endpoint; + $this->identity_url = $endpoint->identity_url; + } +} + +/** + * 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. + * + * identity_url - 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. + * + * 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->endpoint = $endpoint; + $this->identity_url = $endpoint->identity_url; + $this->setup_url = $setup_url; + } +} + +?> diff --git a/lib/Auth/OpenID/CryptUtil.php b/lib/Auth/OpenID/CryptUtil.php new file mode 100644 index 000000000..8d7e06983 --- /dev/null +++ b/lib/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 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +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/lib/Auth/OpenID/DatabaseConnection.php b/lib/Auth/OpenID/DatabaseConnection.php new file mode 100644 index 000000000..3f4515fa5 --- /dev/null +++ b/lib/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 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * 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/lib/Auth/OpenID/DiffieHellman.php b/lib/Auth/OpenID/DiffieHellman.php new file mode 100644 index 000000000..2b845b78b --- /dev/null +++ b/lib/Auth/OpenID/DiffieHellman.php @@ -0,0 +1,181 @@ +<?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 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +require_once 'Auth/OpenID/BigMath.php'; +require_once 'Auth/OpenID/HMACSHA1.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; + } + + /** + * Generate the arguments for an OpenID Diffie-Hellman association + * request + */ + function getAssocArgs() + { + $cpub = $this->lib->longToBase64($this->getPublicKey()); + $args = array( + 'openid.dh_consumer_public' => $cpub, + 'openid.session_type' => 'DH-SHA1' + ); + + if ($this->lib->cmp($this->mod, Auth_OpenID_getDefaultMod()) || + $this->lib->cmp($this->gen, Auth_OpenID_getDefaultGen())) { + $args['openid.dh_modulus'] = $this->lib->longToBase64($this->mod); + $args['openid.dh_gen'] = $this->lib->longToBase64($this->gen); + } + + return $args; + } + + function usingDefaultValues() + { + return ($this->mod == Auth_OpenID_getDefaultMod() && + $this->gen == Auth_OpenID_getDefaultGen()); + } + + /** + * Perform the server side of the OpenID Diffie-Hellman association + */ + function serverAssociate($consumer_args, $assoc_secret) + { + $lib =& Auth_OpenID_getMathLib(); + + if (isset($consumer_args['openid.dh_modulus'])) { + $mod = $lib->base64ToLong($consumer_args['openid.dh_modulus']); + } else { + $mod = null; + } + + if (isset($consumer_args['openid.dh_gen'])) { + $gen = $lib->base64ToLong($consumer_args['openid.dh_gen']); + } else { + $gen = null; + } + + $cpub64 = @$consumer_args['openid.dh_consumer_public']; + if (!isset($cpub64)) { + return false; + } + + $dh = new Auth_OpenID_DiffieHellman($mod, $gen); + $cpub = $lib->base64ToLong($cpub64); + $mac_key = $dh->xorSecret($cpub, $assoc_secret); + $enc_mac_key = base64_encode($mac_key); + $spub64 = $lib->longToBase64($dh->getPublicKey()); + + $server_args = array( + 'session_type' => 'DH-SHA1', + 'dh_server_public' => $spub64, + 'enc_mac_key' => $enc_mac_key + ); + + return $server_args; + } + + function consumerFinish($reply) + { + $spub = $this->lib->base64ToLong($reply['dh_server_public']); + if ($this->lib->cmp($spub, 0) <= 0) { + return false; + } + $enc_mac_key = base64_decode($reply['enc_mac_key']); + return $this->xorSecret($spub, $enc_mac_key); + } + + function xorSecret($composite, $secret) + { + $dh_shared = $this->getSharedSecret($composite); + $dh_shared_str = $this->lib->longToBinary($dh_shared); + $sha1_dh_shared = Auth_OpenID_SHA1($dh_shared_str); + + $xsecret = ""; + for ($i = 0; $i < strlen($secret); $i++) { + $xsecret .= chr(ord($secret[$i]) ^ ord($sha1_dh_shared[$i])); + } + + return $xsecret; + } +} diff --git a/lib/Auth/OpenID/Discover.php b/lib/Auth/OpenID/Discover.php new file mode 100644 index 000000000..d87d47d16 --- /dev/null +++ b/lib/Auth/OpenID/Discover.php @@ -0,0 +1,258 @@ +<?php + +/** + * The OpenID and Yadis discovery implementation for OpenID 1.2. + */ + +require_once "Auth/OpenID.php"; +require_once "Auth/OpenID/Parse.php"; +require_once "Services/Yadis/XRIRes.php"; +require_once "Services/Yadis/Yadis.php"; + +define('_OPENID_1_0_NS', 'http://openid.net/xmlns/1.0'); +define('_OPENID_1_2_TYPE', 'http://openid.net/signon/1.2'); +define('_OPENID_1_1_TYPE', 'http://openid.net/signon/1.1'); +define('_OPENID_1_0_TYPE', 'http://openid.net/signon/1.0'); + +/** + * Object representing an OpenID service endpoint. + */ +class Auth_OpenID_ServiceEndpoint { + function Auth_OpenID_ServiceEndpoint() + { + $this->identity_url = null; + $this->server_url = null; + $this->type_uris = array(); + $this->delegate = null; + $this->canonicalID = null; + $this->used_yadis = false; // whether this came from an XRDS + } + + function usesExtension($extension_uri) + { + return in_array($extension_uri, $this->type_uris); + } + + function parseService($yadis_url, $uri, $type_uris, $service_element) + { + // Set the state of this object based on the contents of the + // service element. + $this->type_uris = $type_uris; + $this->identity_url = $yadis_url; + $this->server_url = $uri; + $this->delegate = Auth_OpenID_ServiceEndpoint::findDelegate( + $service_element); + $this->used_yadis = true; + } + + function findDelegate($service) + { + // Extract a openid:Delegate value from a Yadis Service + // element. If no delegate is found, returns null. + + // Try to register new namespace. + $service->parser->registerNamespace('openid', + 'http://openid.net/xmlns/1.0'); + + // XXX: should this die if there is more than one delegate + // element? + $delegates = $service->getElements("openid:Delegate"); + + if ($delegates) { + return $service->parser->content($delegates[0]); + } else { + return null; + } + } + + function getServerID() + { + // Return the identifier that should be sent as the + // openid.identity_url parameter to the server. + if ($this->delegate === null) { + if ($this->canonicalID) { + return $this->canonicalID; + } else { + return $this->identity_url; + } + } else { + return $this->delegate; + } + } + + function fromHTML($uri, $html) + { + // Parse the given document as HTML looking for an OpenID <link + // rel=...> + $urls = Auth_OpenID_legacy_discover($html); + if ($urls === false) { + return null; + } + + list($delegate_url, $server_url) = $urls; + + $service = new Auth_OpenID_ServiceEndpoint(); + $service->identity_url = $uri; + $service->delegate = $delegate_url; + $service->server_url = $server_url; + $service->type_uris = array(_OPENID_1_0_TYPE); + return $service; + } +} + +function filter_MatchesAnyOpenIDType(&$service) +{ + $uris = $service->getTypes(); + + foreach ($uris as $uri) { + if (in_array($uri, + array(_OPENID_1_0_TYPE, + _OPENID_1_1_TYPE, + _OPENID_1_2_TYPE))) { + return true; + } + } + + return false; +} + +function Auth_OpenID_makeOpenIDEndpoints($uri, $endpoints) +{ + $s = array(); + + if (!$endpoints) { + return $s; + } + + foreach ($endpoints 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(); + $openid_endpoint->parseService($uri, + $service_uri, + $type_uris, + $service); + + $s[] = $openid_endpoint; + } + } + } + + return $s; +} + +function Auth_OpenID_discoverWithYadis($uri, &$fetcher) +{ + // 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. + $openid_services = array(); + + $http_response = null; + $response = Services_Yadis_Yadis::discover($uri, $http_response, + $fetcher); + + if ($response) { + $identity_url = $response->uri; + $openid_services = + $response->xrds->services(array('filter_MatchesAnyOpenIDType')); + } + + if (!$openid_services) { + return @Auth_OpenID_discoverWithoutYadis($uri, + $fetcher); + } + + if (!$openid_services) { + $body = $response->body; + + // Try to parse the response as HTML to get OpenID 1.0/1.1 + // <link rel="..."> + $service = Auth_OpenID_ServiceEndpoint::fromHTML($identity_url, + $body); + + if ($service !== null) { + $openid_services = array($service); + } + } else { + $openid_services = Auth_OpenID_makeOpenIDEndpoints($response->uri, + $openid_services); + } + + return array($identity_url, $openid_services, $http_response); +} + +function _Auth_OpenID_discoverServiceList($uri, &$fetcher) +{ + list($url, $services, $resp) = Auth_OpenID_discoverWithYadis($uri, + $fetcher); + + return $services; +} + +function _Auth_OpenID_discoverXRIServiceList($uri, &$fetcher) +{ + list($url, $services, $resp) = _Auth_OpenID_discoverXRI($uri, + $fetcher); + return $services; +} + +function Auth_OpenID_discoverWithoutYadis($uri, &$fetcher) +{ + $http_resp = @$fetcher->get($uri); + + if ($http_resp->status != 200) { + return array(null, array(), $http_resp); + } + + $identity_url = $http_resp->final_url; + + // Try to parse the response as HTML to get OpenID 1.0/1.1 <link + // rel="..."> + $endpoint =& new Auth_OpenID_ServiceEndpoint(); + $service = $endpoint->fromHTML($identity_url, $http_resp->body); + if ($service === null) { + $openid_services = array(); + } else { + $openid_services = array($service); + } + + return array($identity_url, $openid_services, $http_resp); +} + +function _Auth_OpenID_discoverXRI($iname, &$fetcher) +{ + $services = new Services_Yadis_ProxyResolver($fetcher); + list($canonicalID, $service_list) = $services->query($iname, + array(_OPENID_1_0_TYPE, + _OPENID_1_1_TYPE, + _OPENID_1_2_TYPE), + array('filter_MatchesAnyOpenIDType')); + + $endpoints = Auth_OpenID_makeOpenIDEndpoints($iname, $service_list); + + for ($i = 0; $i < count($endpoints); $i++) { + $endpoints[$i]->canonicalID = $canonicalID; + } + + // FIXME: returned xri should probably be in some normal form + return array($iname, $endpoints, null); +} + +function Auth_OpenID_discover($uri, &$fetcher) +{ + return @Auth_OpenID_discoverWithYadis($uri, $fetcher); +} + +?> \ No newline at end of file diff --git a/lib/Auth/OpenID/DumbStore.php b/lib/Auth/OpenID/DumbStore.php new file mode 100644 index 000000000..d4d8a8b42 --- /dev/null +++ b/lib/Auth/OpenID/DumbStore.php @@ -0,0 +1,116 @@ +<?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 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * Import the interface for creating a new store class. + */ +require_once 'Auth/OpenID/Interface.php'; +require_once 'Auth/OpenID/HMACSHA1.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; + } + + /** + * This implementation does nothing. + */ + function storeNonce($nonce) + { + } + + /** + * 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($nonce) + { + return true; + } + + /** + * This method returns the auth key generated by the constructor. + */ + function getAuthKey() + { + return $this->auth_key; + } + + /** + * This store is a dumb mode store, so this method is overridden + * to return true. + */ + function isDumb() + { + return true; + } +} + +?> \ No newline at end of file diff --git a/lib/Auth/OpenID/FileStore.php b/lib/Auth/OpenID/FileStore.php new file mode 100644 index 000000000..6ce88568e --- /dev/null +++ b/lib/Auth/OpenID/FileStore.php @@ -0,0 +1,674 @@ +<?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 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + * + */ + +/** + * Require base class for creating a new interface. + */ +require_once 'Auth/OpenID.php'; +require_once 'Auth/OpenID/Interface.php'; +require_once 'Auth/OpenID/HMACSHA1.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 and the $directory containing the auth key file. + $this->temp_dir = $directory . DIRECTORY_SEPARATOR . 'temp'; + + $this->auth_key_name = $directory . DIRECTORY_SEPARATOR . 'auth_key'; + + $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(dirname($this->auth_key_name)) && + 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->auth_key_name and $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); + } + } + + /** + * Read the auth key from the auth key file. Will return None if + * there is currently no key. + * + * @return mixed + */ + function readAuthKey() + { + if (!$this->active) { + trigger_error("FileStore no longer active", E_USER_ERROR); + return null; + } + + $auth_key_file = @fopen($this->auth_key_name, 'rb'); + if ($auth_key_file === false) { + return null; + } + + $key = fread($auth_key_file, filesize($this->auth_key_name)); + fclose($auth_key_file); + + return $key; + } + + /** + * Generate a new random auth key and safely store it in the + * location specified by $this->auth_key_name. + * + * @return string $key + */ + function createAuthKey() + { + if (!$this->active) { + trigger_error("FileStore no longer active", E_USER_ERROR); + return null; + } + + $auth_key = Auth_OpenID_CryptUtil::randomString($this->AUTH_KEY_LEN); + + list($file_obj, $tmp) = $this->_mktemp(); + + fwrite($file_obj, $auth_key); + fflush($file_obj); + fclose($file_obj); + + if (function_exists('link')) { + // Posix filesystem + $saved = link($tmp, $this->auth_key_name); + Auth_OpenID_FileStore::_removeIfPresent($tmp); + } else { + // Windows filesystem + $saved = rename($tmp, $this->auth_key_name); + } + + if (!$saved) { + // The link failed, either because we lack the permission, + // or because the file already exists; try to read the key + // in case the file already existed. + $auth_key = $this->readAuthKey(); + } + + return $auth_key; + } + + /** + * Retrieve the auth key from the file specified by + * $this->auth_key_name, creating it if it does not exist. + * + * @return string $key + */ + function getAuthKey() + { + if (!$this->active) { + trigger_error("FileStore no longer active", E_USER_ERROR); + return null; + } + + $auth_key = $this->readAuthKey(); + if ($auth_key === null) { + $auth_key = $this->createAuthKey(); + + if (strlen($auth_key) != $this->AUTH_KEY_LEN) { + $fmt = 'Got an invalid auth key from %s. Expected '. + '%d-byte string. Got: %s'; + $msg = sprintf($fmt, $this->auth_key_name, $this->AUTH_KEY_LEN, + $auth_key); + trigger_error($msg, E_USER_WARNING); + return null; + } + } + return $auth_key; + } + + /** + * 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) { + if (strpos($association_file, $name) === 0) { + $matching_files[] = $association_file; + } + } + + $matching_associations = array(); + // read the matching files and sort by time issued + foreach ($matching_files as $name) { + $full_name = $this->association_dir . DIRECTORY_SEPARATOR . + $name; + $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); + } + } + + /** + * Mark this nonce as present. + */ + function storeNonce($nonce) + { + if (!$this->active) { + trigger_error("FileStore no longer active", E_USER_ERROR); + return null; + } + + $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $nonce; + $nonce_file = fopen($filename, 'w'); + if ($nonce_file === false) { + return false; + } + fclose($nonce_file); + return true; + } + + /** + * Return whether this nonce is present. As a side effect, mark it + * as no longer present. + * + * @return bool $present + */ + function useNonce($nonce) + { + if (!$this->active) { + trigger_error("FileStore no longer active", E_USER_ERROR); + return null; + } + + $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $nonce; + $st = @stat($filename); + + if ($st === false) { + return false; + } + + // Either it is too old or we are using it. Either way, we + // must remove the file. + if (!unlink($filename)) { + return false; + } + + $now = time(); + $nonce_age = $now - $st[9]; + + // We can us it if the age of the file is less than the + // expiration time. + return $nonce_age <= $this->max_nonce_age; + } + + /** + * Remove expired entries from the database. This is potentially + * expensive, so only run when it is acceptable to take time. + */ + 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) { + $filename = $this->nonce_dir . DIRECTORY_SEPARATOR . $nonce; + $st = @stat($filename); + + if ($st !== false) { + // Remove the nonce if it has expired + $nonce_age = $now - $st[9]; + if ($nonce_age > $this->max_nonce_age) { + Auth_OpenID_FileStore::_removeIfPresent($filename); + } + } + } + + $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) { + Auth_OpenID_FileStore::_removeIfPresent( + $association_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))) { + $files[] = $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 = ""; + for ($i = 0; $i < strlen($str); $i++) { + $c = $str[$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); + } +} + +?> diff --git a/lib/Auth/OpenID/HMACSHA1.php b/lib/Auth/OpenID/HMACSHA1.php new file mode 100644 index 000000000..6daadb5f8 --- /dev/null +++ b/lib/Auth/OpenID/HMACSHA1.php @@ -0,0 +1,72 @@ +<?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 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * SHA1_BLOCKSIZE is this module's SHA1 blocksize used by the fallback + * implementation. + */ +define('Auth_OpenID_SHA1_BLOCKSIZE', 64); + +if (!function_exists('sha1')) { + /** + * Return a raw SHA1 hash of the given string + * + * XXX: include the SHA1 code from Dan Libby's OpenID library + */ + function Auth_OpenID_SHA1($text) + { + trigger_error('No SHA1 function found', E_USER_ERROR); + } +} else { + /** + * @ignore + */ + function Auth_OpenID_SHA1($text) + { + $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; + } +} + +/** + * 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 (strlen($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; +} + +?> \ No newline at end of file diff --git a/lib/Auth/OpenID/Interface.php b/lib/Auth/OpenID/Interface.php new file mode 100644 index 000000000..ce9fa1feb --- /dev/null +++ b/lib/Auth/OpenID/Interface.php @@ -0,0 +1,188 @@ +<?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 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * 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. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + */ +class Auth_OpenID_OpenIDStore { + /** + * @var integer The length of the auth key that should be returned + * by the getAuthKey method. + */ + var $AUTH_KEY_LEN = 20; + + /** + * 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); + } + + /** + * 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 + * that will remain valid for the longest duration. + * + * 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); + } + + /** + * Stores a nonce. This is used by the consumer to prevent replay + * attacks. + * + * @param string $nonce The nonce to store. + * + * @return null + */ + function storeNonce($nonce) + { + trigger_error("Auth_OpenID_OpenIDStore::storeNonce ". + "not implemented", E_USER_ERROR); + } + + /** + * This method is called when the library is attempting to use a + * nonce. If the nonce is in the store, this method removes it and + * returns a value which evaluates as true. Otherwise it returns a + * value which evaluates as false. + * + * This method is allowed and encouraged to treat nonces older + * than some period (a very conservative window would be 6 hours, + * for example) as no longer existing, and return False and remove + * them. + * + * @param string $nonce The nonce to use. + * + * @return bool Whether or not the nonce was valid. + */ + function useNonce($nonce) + { + trigger_error("Auth_OpenID_OpenIDStore::useNonce ". + "not implemented", E_USER_ERROR); + } + + /** + * This method returns a key used to sign the tokens, to ensure + * that they haven't been tampered with in transit. It should + * return the same key every time it is called. The key returned + * should be {@link AUTH_KEY_LEN} bytes long. + * + * @return string The key. It should be {@link AUTH_KEY_LEN} bytes in + * length, and use the full range of byte values. That is, it + * should be treated as a lump of binary data stored in a string. + */ + function getAuthKey() + { + trigger_error("Auth_OpenID_OpenIDStore::getAuthKey ". + "not implemented", E_USER_ERROR); + } + + /** + * This method must return true if the store is a dumb-mode-style + * store. Unlike all other methods in this class, this one + * provides a default implementation, which returns false. + * + * In general, any custom subclass of {@link Auth_OpenID_OpenIDStore} + * won't override this method, as custom subclasses are only likely to + * be created when the store is fully functional. + * + * @return bool true if the store works fully, false if the + * consumer will have to use dumb mode to use this store. + */ + function isDumb() + { + return false; + } + + /** + * Removes all entries from the store; implementation is optional. + */ + function reset() + { + } + +} +?> \ No newline at end of file diff --git a/lib/Auth/OpenID/KVForm.php b/lib/Auth/OpenID/KVForm.php new file mode 100644 index 000000000..6075c44f0 --- /dev/null +++ b/lib/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 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * 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/lib/Auth/OpenID/MySQLStore.php b/lib/Auth/OpenID/MySQLStore.php new file mode 100644 index 000000000..f89afc6fe --- /dev/null +++ b/lib/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 (nonce CHAR(8) UNIQUE PRIMARY KEY, ". + "expires INTEGER) TYPE=InnoDB"; + + $this->sql['assoc_table'] = + "CREATE TABLE %s (server_url BLOB, handle VARCHAR(255), ". + "secret BLOB, issued INTEGER, lifetime INTEGER, ". + "assoc_type VARCHAR(64), PRIMARY KEY (server_url(255), handle)) ". + "TYPE=InnoDB"; + + $this->sql['settings_table'] = + "CREATE TABLE %s (setting VARCHAR(128) UNIQUE PRIMARY KEY, ". + "value BLOB) TYPE=InnoDB"; + + $this->sql['create_auth'] = + "INSERT INTO %s VALUES ('auth_key', !)"; + + $this->sql['get_auth'] = + "SELECT value FROM %s WHERE setting = 'auth_key'"; + + $this->sql['set_assoc'] = + "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'] = + "REPLACE INTO %s (nonce, expires) VALUES (?, ?)"; + + $this->sql['get_nonce'] = + "SELECT * FROM %s WHERE nonce = ?"; + + $this->sql['remove_nonce'] = + "DELETE FROM %s WHERE nonce = ?"; + } + + /** + * @access private + */ + function blobEncode($blob) + { + return "0x" . bin2hex($blob); + } +} + +?> \ No newline at end of file diff --git a/lib/Auth/OpenID/Parse.php b/lib/Auth/OpenID/Parse.php new file mode 100644 index 000000000..891ca5e71 --- /dev/null +++ b/lib/Auth/OpenID/Parse.php @@ -0,0 +1,308 @@ +<?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 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * 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\/<>]+)'; + + 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) + { + if ($close_tags) { + $options = implode("|", array_merge(array($tag_name), $close_tags)); + $closer = sprintf("(?:%s)", $options); + } else { + $closer = $tag_name; + } + + $expr = sprintf($this->_tag_expr, $tag_name, $closer); + return sprintf("/%s/%s", $expr, $this->_re_flags); + } + + function htmlFind() + { + return $this->tagMatcher('html'); + } + + function headFind() + { + return $this->tagMatcher('head', array('body')); + } + + 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); + + // Try to find the <HTML> tag. + $html_re = $this->htmlFind(); + $html_matches = array(); + if (!preg_match($html_re, $stripped, $html_matches)) { + return array(); + } + + // Try to find the <HEAD> tag. + $head_re = $this->headFind(); + $head_matches = array(); + if (!preg_match($head_re, $html_matches[0], $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) +{ + $p = new Auth_OpenID_Parse(); + + $link_attrs = $p->parseLinkAttrs($html_text); + + $server_url = $p->findFirstHref($link_attrs, + 'openid.server'); + + if ($server_url === null) { + return false; + } else { + $delegate_url = $p->findFirstHref($link_attrs, + 'openid.delegate'); + return array($delegate_url, $server_url); + } +} + +?> \ No newline at end of file diff --git a/lib/Auth/OpenID/PostgreSQLStore.php b/lib/Auth/OpenID/PostgreSQLStore.php new file mode 100644 index 000000000..a41528078 --- /dev/null +++ b/lib/Auth/OpenID/PostgreSQLStore.php @@ -0,0 +1,136 @@ +<?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 (nonce CHAR(8) UNIQUE PRIMARY KEY, ". + "expires INTEGER)"; + + $this->sql['assoc_table'] = + "CREATE TABLE %s (server_url VARCHAR(2047), handle VARCHAR(255), ". + "secret BYTEA, issued INTEGER, lifetime INTEGER, ". + "assoc_type VARCHAR(64), PRIMARY KEY (server_url, handle), ". + "CONSTRAINT secret_length_constraint CHECK ". + "(LENGTH(secret) <= 128))"; + + $this->sql['settings_table'] = + "CREATE TABLE %s (setting VARCHAR(128) UNIQUE PRIMARY KEY, ". + "value BYTEA, ". + "CONSTRAINT value_length_constraint CHECK (LENGTH(value) <= 20))"; + + $this->sql['create_auth'] = + "INSERT INTO %s VALUES ('auth_key', '!')"; + + $this->sql['get_auth'] = + "SELECT value FROM %s WHERE setting = 'auth_key'"; + + $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'] = + array( + 'insert_nonce' => "INSERT INTO %s (nonce, expires) VALUES ". + "(?, ?)", + 'update_nonce' => "UPDATE %s SET expires = ? WHERE nonce = ?" + ); + + $this->sql['get_nonce'] = + "SELECT * FROM %s WHERE nonce = ?"; + + $this->sql['remove_nonce'] = + "DELETE FROM %s WHERE nonce = ?"; + } + + /** + * @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 _add_nonce($nonce, $expires) + { + if ($this->_get_nonce($nonce)) { + return $this->resultToBool($this->connection->query( + $this->sql['add_nonce']['update_nonce'], + array($expires, $nonce))); + } else { + return $this->resultToBool($this->connection->query( + $this->sql['add_nonce']['insert_nonce'], + array($nonce, $expires))); + } + } + + /** + * @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/lib/Auth/OpenID/SQLStore.php b/lib/Auth/OpenID/SQLStore.php new file mode 100644 index 000000000..c7bd5401f --- /dev/null +++ b/lib/Auth/OpenID/SQLStore.php @@ -0,0 +1,658 @@ +<?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 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * 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'; + +/** + * 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 + * settings_table_name, 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 string $settings_table This is an optional parameter to + * specify the name of the table used for this store's settings. + * The default value is 'oid_settings'. + * + * @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, $settings_table = null, + $associations_table = null, + $nonces_table = null) + { + global $__Auth_OpenID_PEAR_AVAILABLE; + + $this->settings_table_name = "oid_settings"; + $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 ($settings_table) { + $this->settings_table_name = $settings_table; + } + + 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("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)); + + $this->connection->query(sprintf("DELETE FROM %s", + $this->settings_table_name)); + } + + /** + * @access private + */ + function _verifySQL() + { + $missing = array(); + $empty = array(); + + $required_sql_keys = array( + 'nonce_table', + 'assoc_table', + 'settings_table', + 'get_auth', + 'create_auth', + 'set_assoc', + 'get_assoc', + 'get_assocs', + 'remove_assoc', + 'add_nonce', + 'get_nonce', + 'remove_nonce' + ); + + 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', + 'get_nonce', + 'remove_nonce') + ), + array( + 'value' => $this->associations_table_name, + 'keys' => array('assoc_table', + 'set_assoc', + 'get_assoc', + 'get_assocs', + 'remove_assoc') + ), + array( + 'value' => $this->settings_table_name, + 'keys' => array('settings_table', + 'get_auth', + 'create_auth') + ) + ); + + 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(); + $s = $this->create_settings_table(); + $this->connection->autoCommit(false); + + if ($n && $a && $s) { + 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; + } + + function create_settings_table() + { + if (!$this->tableExists($this->settings_table_name)) { + $r = $this->connection->query($this->sql['settings_table']); + return $this->resultToBool($r); + } + return true; + } + + /** + * @access private + */ + function _get_auth() + { + return $this->connection->getOne($this->sql['get_auth']); + } + + /** + * @access private + */ + function _create_auth($str) + { + return $this->connection->query($this->sql['create_auth'], + array($str)); + } + + function getAuthKey() + { + $value = $this->_get_auth(); + if (!$value) { + $auth_key = + Auth_OpenID_CryptUtil::randomString($this->AUTH_KEY_LEN); + + $auth_key_s = $this->blobEncode($auth_key); + $this->_create_auth($auth_key_s); + } else { + $auth_key_s = $value; + $auth_key = $this->blobDecode($auth_key_s); + } + + $this->connection->commit(); + + if (strlen($auth_key) != $this->AUTH_KEY_LEN) { + $fmt = "Expected %d-byte string for auth key. Got key of length %d"; + trigger_error(sprintf($fmt, $this->AUTH_KEY_LEN, strlen($auth_key)), + E_USER_WARNING); + return null; + } + + return $auth_key; + } + + /** + * @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($nonce, $expires) + { + $sql = $this->sql['add_nonce']; + $result = $this->connection->query($sql, array($nonce, $expires)); + return $this->resultToBool($result); + } + + /** + * @access private + */ + function storeNonce($nonce) + { + if ($this->_add_nonce($nonce, time())) { + $this->connection->commit(); + } else { + $this->connection->rollback(); + } + } + + /** + * @access private + */ + function _get_nonce($nonce) + { + $result = $this->connection->getRow($this->sql['get_nonce'], + array($nonce)); + + if ($this->isError($result)) { + return null; + } else { + return $result; + } + } + + /** + * @access private + */ + function _remove_nonce($nonce) + { + $this->connection->query($this->sql['remove_nonce'], + array($nonce)); + } + + function useNonce($nonce) + { + $row = $this->_get_nonce($nonce); + + if ($row !== null) { + $nonce = $row['nonce']; + $timestamp = $row['expires']; + $nonce_age = time() - $timestamp; + + if ($nonce_age > $this->max_nonce_age) { + $present = 0; + } else { + $present = 1; + } + + $this->_remove_nonce($nonce); + } else { + $present = 0; + } + + $this->connection->commit(); + + return $present; + } + + /** + * "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 < strlen($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; + } +} + +?> diff --git a/lib/Auth/OpenID/SQLiteStore.php b/lib/Auth/OpenID/SQLiteStore.php new file mode 100644 index 000000000..6351df3ca --- /dev/null +++ b/lib/Auth/OpenID/SQLiteStore.php @@ -0,0 +1,66 @@ +<?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 (nonce CHAR(8) UNIQUE PRIMARY KEY, ". + "expires INTEGER)"; + + $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['settings_table'] = + "CREATE TABLE %s (setting VARCHAR(128) UNIQUE PRIMARY KEY, ". + "value BLOB(20))"; + + $this->sql['create_auth'] = + "INSERT INTO %s VALUES ('auth_key', ?)"; + + $this->sql['get_auth'] = + "SELECT value FROM %s WHERE setting = 'auth_key'"; + + $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 OR REPLACE INTO %s (nonce, expires) VALUES (?, ?)"; + + $this->sql['get_nonce'] = + "SELECT * FROM %s WHERE nonce = ?"; + + $this->sql['remove_nonce'] = + "DELETE FROM %s WHERE nonce = ?"; + } +} + +?> \ No newline at end of file diff --git a/lib/Auth/OpenID/Server.php b/lib/Auth/OpenID/Server.php new file mode 100644 index 000000000..b82bb4acb --- /dev/null +++ b/lib/Auth/OpenID/Server.php @@ -0,0 +1,1307 @@ +<?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 + * 'decodeRequest' and 'encodeResponse' methods of the + * Auth_OpenID_Server object. 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 Auth_OpenID_CheckIDRequest object, + * and if and when you've come to a decision, form a response by + * calling Auth_OpenID_CheckIDRequest::answer. + * + * Other types of requests relate to establishing associations between + * client and server and verifing the authenticity of previous + * communications. Auth_OpenID_Server contains all the logic and data + * necessary to respond to such requests; just pass it to + * 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 Auth_OpenID_Request class here. But you'll likely want data + * sent back by your extension to be signed. Auth_OpenID_Response + * provides methods with which you can add data to it which can be + * signed with the other data in the OpenID signature. + * + * For example: + * + * // 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') + * + * 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 Auth_OpenID_FileStore. For stores based on + * MySQL, SQLite, or PostgreSQL, see the 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 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * 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"; + +define('AUTH_OPENID_HTTP_OK', 200); +define('AUTH_OPENID_HTTP_REDIRECT', 302); +define('AUTH_OPENID_HTTP_ERROR', 400); + +global $_Auth_OpenID_Request_Modes, + $_Auth_OpenID_OpenID_Prefix, + $_Auth_OpenID_Encode_Kvform, + $_Auth_OpenID_Encode_Url; + +/** + * @access private + */ +$_Auth_OpenID_Request_Modes = array('checkid_setup', + 'checkid_immediate'); + +/** + * @access private + */ +$_Auth_OpenID_OpenID_Prefix = "openid."; + +/** + * @access private + */ +$_Auth_OpenID_Encode_Kvform = array('kfvorm'); + +/** + * @access private + */ +$_Auth_OpenID_Encode_Url = array('URL/redirect'); + +/** + * @access private + */ +function _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($query = null, $message = null) + { + $this->message = $message; + $this->query = $query; + } + + /** + * Returns the return_to URL for the request which caused this + * error. + */ + function hasReturnTo() + { + global $_Auth_OpenID_OpenID_Prefix; + if ($this->query) { + return array_key_exists($_Auth_OpenID_OpenID_Prefix . + 'return_to', $this->query); + } else { + return false; + } + } + + /** + * 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() + { + global $_Auth_OpenID_OpenID_Prefix; + $return_to = Auth_OpenID::arrayGet($this->query, + $_Auth_OpenID_OpenID_Prefix . + 'return_to'); + if (!$return_to) { + return new Auth_OpenID_ServerError(null, "no return_to URL"); + } + + return Auth_OpenID::appendArgs($return_to, + array('openid.mode' => 'error', + 'openid.error' => $this->toString())); + } + + /** + * 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())); + } + + /** + * Returns one of $_Auth_OpenID_Encode_Url, + * $_Auth_OpenID_Encode_Kvform, or null, depending on the type of + * encoding expected for this error's payload. + */ + function whichEncoding() + { + global $_Auth_OpenID_Encode_Url, + $_Auth_OpenID_Encode_Kvform, + $_Auth_OpenID_Request_Modes; + + if ($this->hasReturnTo()) { + return $_Auth_OpenID_Encode_Url; + } + + $mode = Auth_OpenID::arrayGet($this->query, 'openid.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->message) { + return $this->message; + } else { + return get_class($this) . " error"; + } + } +} + +/** + * An error indicating that the return_to URL is malformed. + * + * @package OpenID + */ +class Auth_OpenID_MalformedReturnURL extends Auth_OpenID_ServerError { + function Auth_OpenID_MalformedReturnURL($query, $return_to) + { + $this->return_to = $return_to; + parent::Auth_OpenID_ServerError($query, "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 toString() + { + return "Malformed trust root"; + } +} + +/** + * The base class for all server request classes. + * + * @access private + * @package OpenID + */ +class Auth_OpenID_Request { + var $mode = null; +} + +/** + * A request to verify the validity of a previous response. + * + * @access private + * @package OpenID + */ +class Auth_OpenID_CheckAuthRequest extends Auth_OpenID_Request { + var $mode = "check_authentication"; + var $invalidate_handle = null; + + function Auth_OpenID_CheckAuthRequest($assoc_handle, $sig, $signed, + $invalidate_handle = null) + { + $this->assoc_handle = $assoc_handle; + $this->sig = $sig; + $this->signed = $signed; + if ($invalidate_handle !== null) { + $this->invalidate_handle = $invalidate_handle; + } + } + + function fromQuery($query) + { + global $_Auth_OpenID_OpenID_Prefix; + + $required_keys = array('assoc_handle', 'sig', 'signed'); + + foreach ($required_keys as $k) { + if (!array_key_exists($_Auth_OpenID_OpenID_Prefix . $k, + $query)) { + return new Auth_OpenID_ServerError($query, + sprintf("%s request missing required parameter %s from \ + query", "check_authentication", $k)); + } + } + + $assoc_handle = $query[$_Auth_OpenID_OpenID_Prefix . 'assoc_handle']; + $sig = $query[$_Auth_OpenID_OpenID_Prefix . 'sig']; + $signed_list = $query[$_Auth_OpenID_OpenID_Prefix . 'signed']; + + $signed_list = explode(",", $signed_list); + $signed_pairs = array(); + + foreach ($signed_list as $field) { + if ($field == 'mode') { + // XXX KLUDGE HAX WEB PROTOCoL BR0KENNN + // + // openid.mode is currently check_authentication + // because that's the mode of this request. But the + // signature was made on something with a different + // openid.mode. + $value = "id_res"; + } else { + if (array_key_exists($_Auth_OpenID_OpenID_Prefix . $field, + $query)) { + $value = $query[$_Auth_OpenID_OpenID_Prefix . $field]; + } else { + return new Auth_OpenID_ServerError($query, + sprintf("Couldn't find signed field %r in query %s", + $field, var_export($query, true))); + } + } + $signed_pairs[] = array($field, $value); + } + + $result = new Auth_OpenID_CheckAuthRequest($assoc_handle, $sig, + $signed_pairs); + $result->invalidate_handle = Auth_OpenID::arrayGet($query, + $_Auth_OpenID_OpenID_Prefix . 'invalidate_handle'); + return $result; + } + + function answer(&$signatory) + { + $is_valid = $signatory->verify($this->assoc_handle, $this->sig, + $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['is_valid'] = $is_valid ? "true" : "false"; + + if ($this->invalidate_handle) { + $assoc = $signatory->getAssociation($this->invalidate_handle, + false); + if (!$assoc) { + $response->fields['invalidate_handle'] = + $this->invalidate_handle; + } + } + return $response; + } +} + +class Auth_OpenID_PlainTextServerSession { + /** + * An object that knows how to handle association requests with no + * session type. + */ + var $session_type = 'plaintext'; + + function fromQuery($unused_request) + { + return new Auth_OpenID_PlainTextServerSession(); + } + + function answer($secret) + { + return array('mac_key' => base64_encode($secret)); + } +} + +class Auth_OpenID_DiffieHellmanServerSession { + /** + * An object that knows how to handle association requests with + * the Diffie-Hellman session type. + */ + + var $session_type = 'DH-SHA1'; + + function Auth_OpenID_DiffieHellmanServerSession($dh, $consumer_pubkey) + { + $this->dh = $dh; + $this->consumer_pubkey = $consumer_pubkey; + } + + function fromQuery($query) + { + $dh_modulus = Auth_OpenID::arrayGet($query, 'openid.dh_modulus'); + $dh_gen = Auth_OpenID::arrayGet($query, 'openid.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( + '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( + $query, "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 = Auth_OpenID::arrayGet($query, + 'openid.dh_consumer_public'); + if ($consumer_pubkey === null) { + return new Auth_OpenID_ServerError( + '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($query, + "dh_consumer_public is not base64"); + } + + return new Auth_OpenID_DiffieHellmanServerSession($dh, + $consumer_pubkey); + } + + function answer($secret) + { + $lib =& Auth_OpenID_getMathLib(); + $mac_key = $this->dh->xorSecret($this->consumer_pubkey, $secret); + return array( + 'dh_server_public' => + $lib->longToBase64($this->dh->public), + 'enc_mac_key' => base64_encode($mac_key)); + } +} + +/** + * A request to associate with the server. + * + * @access private + * @package OpenID + */ +class Auth_OpenID_AssociateRequest extends Auth_OpenID_Request { + var $mode = "associate"; + var $assoc_type = 'HMAC-SHA1'; + + function Auth_OpenID_AssociateRequest(&$session) + { + $this->session =& $session; + } + + function fromQuery($query) + { + global $_Auth_OpenID_OpenID_Prefix; + + $session_classes = array( + 'DH-SHA1' => 'Auth_OpenID_DiffieHellmanServerSession', + null => 'Auth_OpenID_PlainTextServerSession'); + + $session_type = null; + + if (array_key_exists($_Auth_OpenID_OpenID_Prefix . 'session_type', + $query)) { + $session_type = $query[$_Auth_OpenID_OpenID_Prefix . + 'session_type']; + } + + if (!array_key_exists($session_type, $session_classes)) { + return new Auth_OpenID_ServerError($query, + "Unknown session type $session_type"); + } + + $session_cls = $session_classes[$session_type]; + $session = call_user_func_array(array($session_cls, 'fromQuery'), + array($query)); + + if (($session === null) || (_isError($session))) { + return new Auth_OpenID_ServerError($query, + "Error parsing $session_type session"); + } + + return new Auth_OpenID_AssociateRequest($session); + } + + function answer($assoc) + { + $ml =& Auth_OpenID_getMathLib(); + $response = new Auth_OpenID_ServerResponse($this); + + $response->fields = array('expires_in' => $assoc->getExpiresIn(), + 'assoc_type' => 'HMAC-SHA1', + 'assoc_handle' => $assoc->handle); + + $r = $this->session->answer($assoc->secret); + foreach ($r as $k => $v) { + $response->fields[$k] = $v; + } + + if ($this->session->session_type != 'plaintext') { + $response->fields['session_type'] = $this->session->session_type; + } + + return $response; + } +} + +/** + * A request to confirm the identity of a user. + * + * @access private + * @package OpenID + */ +class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { + var $mode = "checkid_setup"; // or "checkid_immediate" + var $immediate = false; + var $trust_root = null; + + function make($query, $identity, $return_to, $trust_root = null, + $immediate = false, $assoc_handle = null) + { + if (!Auth_OpenID_TrustRoot::_parse($return_to)) { + return new Auth_OpenID_MalformedReturnURL($query, $return_to); + } + + $r = new Auth_OpenID_CheckIDRequest($identity, $return_to, + $trust_root, $immediate, + $assoc_handle); + + if (!$r->trustRootValid()) { + return new Auth_OpenID_UntrustedReturnURL($return_to, + $trust_root); + } else { + return $r; + } + } + + function Auth_OpenID_CheckIDRequest($identity, $return_to, + $trust_root = null, $immediate = false, + $assoc_handle = null) + { + $this->identity = $identity; + $this->return_to = $return_to; + $this->trust_root = $trust_root; + $this->assoc_handle = $assoc_handle; + + if ($immediate) { + $this->immediate = true; + $this->mode = "checkid_immediate"; + } else { + $this->immediate = false; + $this->mode = "checkid_setup"; + } + } + + function fromQuery($query) + { + global $_Auth_OpenID_OpenID_Prefix; + + $mode = $query[$_Auth_OpenID_OpenID_Prefix . 'mode']; + $immediate = null; + + if ($mode == "checkid_immediate") { + $immediate = true; + $mode = "checkid_immediate"; + } else { + $immediate = false; + $mode = "checkid_setup"; + } + + $required = array('identity', + 'return_to'); + + $optional = array('trust_root', + 'assoc_handle'); + + $values = array(); + + foreach ($required as $field) { + if (array_key_exists($_Auth_OpenID_OpenID_Prefix . $field, + $query)) { + $value = $query[$_Auth_OpenID_OpenID_Prefix . $field]; + } else { + return new Auth_OpenID_ServerError($query, + sprintf("Missing required field %s from request", + $field)); + } + $values[$field] = $value; + } + + foreach ($optional as $field) { + $value = null; + if (array_key_exists($_Auth_OpenID_OpenID_Prefix . $field, + $query)) { + $value = $query[$_Auth_OpenID_OpenID_Prefix. $field]; + } + if ($value) { + $values[$field] = $value; + } + } + + if (!Auth_OpenID_TrustRoot::_parse($values['return_to'])) { + return new Auth_OpenID_MalformedReturnURL($query, + $values['return_to']); + } + + $obj = Auth_OpenID_CheckIDRequest::make($query, + $values['identity'], + $values['return_to'], + Auth_OpenID::arrayGet($values, + 'trust_root', null), + $immediate); + + if (is_a($obj, 'Auth_OpenID_ServerError')) { + return $obj; + } + + if (Auth_OpenID::arrayGet($values, 'assoc_handle')) { + $obj->assoc_handle = $values['assoc_handle']; + } + + return $obj; + } + + function trustRootValid() + { + if (!$this->trust_root) { + return true; + } + + $tr = Auth_OpenID_TrustRoot::_parse($this->trust_root); + if ($tr === false) { + return new Auth_OpenID_MalformedTrustRoot(null, $this->trust_root); + } + + return Auth_OpenID_TrustRoot::match($this->trust_root, + $this->return_to); + } + + function answer($allow, $server_url = null) + { + if ($allow || $this->immediate) { + $mode = 'id_res'; + } else { + $mode = 'cancel'; + } + + $response = new Auth_OpenID_CheckIDResponse($this, $mode); + + if ($allow) { + $response->fields['identity'] = $this->identity; + $response->fields['return_to'] = $this->return_to; + if (!$this->trustRootValid()) { + return new Auth_OpenID_UntrustedReturnURL($this->return_to, + $this->trust_root); + } + } else { + $response->signed = array(); + if ($this->immediate) { + if (!$server_url) { + return new Auth_OpenID_ServerError(null, + 'setup_url is required for $allow=false \ + in immediate mode.'); + } + + $setup_request =& new Auth_OpenID_CheckIDRequest( + $this->identity, + $this->return_to, + $this->trust_root, + false, + $this->assoc_handle); + + $setup_url = $setup_request->encodeToURL($server_url); + + $response->fields['user_setup_url'] = $setup_url; + } + } + + return $response; + } + + function encodeToURL($server_url) + { + global $_Auth_OpenID_OpenID_Prefix; + + // 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, + 'return_to' => $this->return_to); + + if ($this->trust_root) { + $q['trust_root'] = $this->trust_root; + } + + if ($this->assoc_handle) { + $q['assoc_handle'] = $this->assoc_handle; + } + + $_q = array(); + + foreach ($q as $k => $v) { + $_q[$_Auth_OpenID_OpenID_Prefix . $k] = $v; + } + + return Auth_OpenID::appendArgs($server_url, $_q); + } + + function getCancelURL() + { + global $_Auth_OpenID_OpenID_Prefix; + + if ($this->immediate) { + return new Auth_OpenID_ServerError(null, + "Cancel is not an appropriate \ + response to immediate mode \ + requests."); + } + + return Auth_OpenID::appendArgs($this->return_to, + array($_Auth_OpenID_OpenID_Prefix . 'mode' => + 'cancel')); + } +} + +/** + * This class encapsulates the response to an OpenID server request. + * + * @access private + * @package OpenID + */ +class Auth_OpenID_ServerResponse { + + function Auth_OpenID_ServerResponse($request) + { + $this->request = $request; + $this->fields = array(); + } + + function whichEncoding() + { + global $_Auth_OpenID_Encode_Kvform, + $_Auth_OpenID_Request_Modes, + $_Auth_OpenID_Encode_Url; + + if (in_array($this->request->mode, $_Auth_OpenID_Request_Modes)) { + return $_Auth_OpenID_Encode_Url; + } else { + return $_Auth_OpenID_Encode_Kvform; + } + } + + function encodeToURL() + { + global $_Auth_OpenID_OpenID_Prefix; + + $fields = array(); + + foreach ($this->fields as $k => $v) { + $fields[$_Auth_OpenID_OpenID_Prefix . $k] = $v; + } + + return Auth_OpenID::appendArgs($this->request->return_to, $fields); + } + + function encodeToKVForm() + { + return Auth_OpenID_KVForm::fromArray($this->fields); + } +} + +/** + * A response to a checkid request. + * + * @access private + * @package OpenID + */ +class Auth_OpenID_CheckIDResponse extends Auth_OpenID_ServerResponse { + + function Auth_OpenID_CheckIDResponse(&$request, $mode = 'id_res') + { + parent::Auth_OpenID_ServerResponse($request); + $this->fields['mode'] = $mode; + $this->signed = array(); + + if ($mode == 'id_res') { + array_push($this->signed, 'mode', 'identity', 'return_to'); + } + } + + function addField($namespace, $key, $value, $signed = true) + { + if ($namespace) { + $key = sprintf('%s.%s', $namespace, $key); + } + $this->fields[$key] = $value; + if ($signed && !in_array($key, $this->signed)) { + $this->signed[] = $key; + } + } + + function addFields($namespace, $fields, $signed = true) + { + foreach ($fields as $k => $v) { + $this->addField($namespace, $k, $v, $signed); + } + } + + function update($namespace, $other) + { + $namespaced_fields = array(); + + foreach ($other->fields as $k => $v) { + $name = sprintf('%s.%s', $namespace, $k); + + $namespaced_fields[$name] = $v; + } + + $this->fields = array_merge($this->fields, $namespaced_fields); + $this->signed = array_merge($this->signed, $other->signed); + } +} + +/** + * 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, $sig, $signed_pairs) + { + $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; + } + + $expected_sig = base64_encode($assoc->sign($signed_pairs)); + + return $sig == $expected_sig; + } + + /** + * 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); + if (!$assoc) { + // fall back to dumb mode + $signed_response->fields['invalidate_handle'] = $assoc_handle; + $assoc = $this->createAssociation(true); + } + } else { + // dumb mode. + $assoc = $this->createAssociation(true); + } + + $signed_response->fields['assoc_handle'] = $assoc->handle; + $assoc->addSignature($signed_response->signed, + $signed_response->fields, ''); + return $signed_response; + } + + /** + * Make a new association. + */ + function createAssociation($dumb = true, $assoc_type = 'HMAC-SHA1') + { + $secret = Auth_OpenID_CryptUtil::getBytes(20); + $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) + { + 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)) { + $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 Auth_OpenID_Response to an Auth_OpenID_WebResponse. + * + * @package OpenID + */ +class Auth_OpenID_Encoder { + + var $responseFactory = 'Auth_OpenID_WebResponse'; + + /** + * Encode an Auth_OpenID_Response and return an + * Auth_OpenID_WebResponse. + */ + function encode(&$response) + { + global $_Auth_OpenID_Encode_Kvform, + $_Auth_OpenID_Encode_Url; + + $cls = $this->responseFactory; + + $encode_as = $response->whichEncoding(); + if ($encode_as == $_Auth_OpenID_Encode_Kvform) { + $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 { + return new Auth_OpenID_EncodingError($response); + } + return $wr; + } +} + +/** + * Returns true if the given response needs a signature. + * + * @access private + */ +function needsSigning($response) +{ + return (in_array($response->request->mode, array('checkid_setup', + 'checkid_immediate')) && + $response->signed); +} + +/** + * 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 Auth_OpenID_Response and return an + * 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') && + needsSigning($response)) { + + if (!$this->signatory) { + return new Auth_OpenID_ServerError(null, + "Must have a store to sign request"); + } + if (array_key_exists('sig', $response->fields)) { + return new Auth_OpenID_AlreadySigned($response); + } + $response = $this->signatory->sign($response); + } + return parent::encode($response); + } +} + +/** + * Decode an incoming Auth_OpenID_WebResponse into an + * Auth_OpenID_Request. + * + * @package OpenID + */ +class Auth_OpenID_Decoder { + + function Auth_OpenID_Decoder() + { + global $_Auth_OpenID_OpenID_Prefix; + $this->prefix = $_Auth_OpenID_OpenID_Prefix; + + $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; + } + + $myquery = array(); + + foreach ($query as $k => $v) { + if (strpos($k, $this->prefix) === 0) { + $myquery[$k] = $v; + } + } + + if (!$myquery) { + return null; + } + + $mode = Auth_OpenID::arrayGet($myquery, $this->prefix . 'mode'); + if (!$mode) { + return new Auth_OpenID_ServerError($query, + sprintf("No %s mode found in query", $this->prefix)); + } + + $handlerCls = Auth_OpenID::arrayGet($this->handlers, $mode, + $this->defaultDecoder($query)); + + if (!is_a($handlerCls, 'Auth_OpenID_ServerError')) { + return call_user_func_array(array($handlerCls, 'fromQuery'), + array($query)); + } else { + return $handlerCls; + } + } + + function defaultDecoder($query) + { + $mode = $query[$this->prefix . 'mode']; + return new Auth_OpenID_ServerError($query, + sprintf("No decoder for 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($return_to, $trust_root) + { + global $_Auth_OpenID_OpenID_Prefix; + + $query = array( + $_Auth_OpenID_OpenID_Prefix . 'return_to' => $return_to, + $_Auth_OpenID_OpenID_Prefix . 'trust_root' => $trust_root); + + parent::Auth_OpenID_ServerError($query); + } + + function toString() + { + global $_Auth_OpenID_OpenID_Prefix; + + $return_to = $this->query[$_Auth_OpenID_OpenID_Prefix . 'return_to']; + $trust_root = $this->query[$_Auth_OpenID_OpenID_Prefix . 'trust_root']; + + return sprintf("return_to %s not under trust_root %s", + $return_to, $trust_root); + } +} + +/** + * An object that implements the OpenID protocol for a single URL. + * + * Use this object by calling getOpenIDResponse when you get any + * request for the server URL. + * + * @package OpenID + */ +class Auth_OpenID_Server { + function Auth_OpenID_Server(&$store) + { + $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(); + } + + /** + * Handle a request. Given an Auth_OpenID_Request object, call + * the appropriate Auth_OpenID_Server method to process the + * request and generate a response. + * + * @param Auth_OpenID_Request $request An Auth_OpenID_Request + * returned by Auth_OpenID_Server::decodeRequest. + * + * @return Auth_OpenID_Response $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. + * + * @access private + */ + function openid_check_authentication(&$request) + { + return $request->answer($this->signatory); + } + + /** + * The callback for 'associate' messages. + * + * @access private + */ + function openid_associate(&$request) + { + $assoc = $this->signatory->createAssociation(false); + return $request->answer($assoc); + } + + /** + * 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 + * Auth_OpenID_Request object. + */ + function decodeRequest(&$query) + { + return $this->decoder->decode($query); + } +} + +?> diff --git a/lib/Auth/OpenID/ServerRequest.php b/lib/Auth/OpenID/ServerRequest.php new file mode 100644 index 000000000..00728941a --- /dev/null +++ b/lib/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 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * 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/lib/Auth/OpenID/TrustRoot.php b/lib/Auth/OpenID/TrustRoot.php new file mode 100644 index 000000000..88eff295d --- /dev/null +++ b/lib/Auth/OpenID/TrustRoot.php @@ -0,0 +1,243 @@ +<?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 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * 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', + '/\.(com|edu|gov|int|mil|net|org|biz|info|name|museum|coop|aero|ac|' . + 'ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|az|ba|bb|bd|be|bf|bg|' . + 'bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|' . + 'cm|cn|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|' . + 'fi|fj|fk|fm|fo|fr|ga|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|' . + 'gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|' . + 'ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|' . + 'ma|mc|md|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|' . + 'nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|' . + 'ps|pt|pw|py|qa|re|ro|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|' . + 'so|sr|st|sv|sy|sz|tc|td|tf|tg|th|tj|tk|tm|tn|to|tp|tr|tt|tv|tw|tz|' . + 'ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)$/'); + +/** + * A wrapper for trust-root related functions + */ +class Auth_OpenID_TrustRoot { + /** + * 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) + { + $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; + } + + // Return false if the original trust root value has more than + // one port specification. + if (preg_match("/:\/\/[^:]+(:\d+){2,}(\/|$)/", $trust_root)) { + 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, -1) != '/') { + $path .= '/'; + } + } else { + $path = '/'; + } + $parts['path'] = $path; + if (!isset($parts['port'])) { + $parts['port'] = false; + } + 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; + } + + // 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]; + + // Require at least two levels of specificity for non-country + // tlds and three levels for country tlds. + $elements = explode('.', $parts['host']); + $n = count($elements); + if ($parts['wildcard']) { + $n -= 1; + } + if (strlen($tld) == 2) { + $n -= 1; + } + if ($n <= 1) { + return false; + } + 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 (substr($path, 0, strlen($base_path)) != $base_path) { + 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']); + } +} +?> \ No newline at end of file diff --git a/lib/Auth/OpenID/URINorm.php b/lib/Auth/OpenID/URINorm.php new file mode 100644 index 000000000..d1c653ebe --- /dev/null +++ b/lib/Auth/OpenID/URINorm.php @@ -0,0 +1,231 @@ +<?php + +/** + * URI normalization routines. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +require_once 'Services/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})/'; +} + +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(Services_Yadis_getUCSChars(), + Services_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 (Services_Yadis_startswith($path, '../')) { + $path = substr($path, 3); + } else if (Services_Yadis_startswith($path, './')) { + $path = substr($path, 2); + } else if (Services_Yadis_startswith($path, '/./')) { + $path = substr($path, 2); + } else if ($path == '/.') { + $path = '/'; + } else if (Services_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[] = ''; + } + } + + $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/lib/Services/Yadis/HTTPFetcher.php b/lib/Services/Yadis/HTTPFetcher.php new file mode 100644 index 000000000..97940a4d5 --- /dev/null +++ b/lib/Services/Yadis/HTTPFetcher.php @@ -0,0 +1,92 @@ +<?php + +/** + * This module contains the HTTP fetcher interface + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package Yadis + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +class Services_Yadis_HTTPResponse { + function Services_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 Yadis + */ +class Services_Yadis_HTTPFetcher { + + var $timeout = 20; // timeout in seconds. + + /** + * 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); + } + + /** + * 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($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) + { + trigger_error("not implemented", E_USER_ERROR); + } +} + +?> \ No newline at end of file diff --git a/lib/Services/Yadis/Manager.php b/lib/Services/Yadis/Manager.php new file mode 100644 index 000000000..524eea2ae --- /dev/null +++ b/lib/Services/Yadis/Manager.php @@ -0,0 +1,496 @@ +<?php + +/** + * Yadis service manager to be used during yadis-driven authentication + * attempts. + * + * @package Yadis + */ + +/** + * The base session class used by the Services_Yadis_Manager. This + * class wraps the default PHP session machinery and should be + * subclassed if your application doesn't use PHP sessioning. + * + * @package Yadis + */ +class Services_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. + */ +class Services_Yadis_SessionLoader { + /** + * Override this. + */ + 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. + */ + 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). + */ + 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. + */ + 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. + */ + 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. + */ + function prepareForSave($obj) + { + return array(); + } +} + +class Auth_OpenID_ServiceEndpointLoader extends Services_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']); + } +} + +class Services_Yadis_ManagerLoader extends Services_Yadis_SessionLoader { + function requiredKeys() + { + return array('starting_url', + 'yadis_url', + 'services', + 'session_key', + '_current', + 'stale'); + } + + function newObject($data) + { + return new Services_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 Yadis + */ +class Services_Yadis_Manager { + + /** + * Intialize a new yadis service manager. + * + * @access private + */ + function Services_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 .finish() to clean up the + * session state. + * + * @package Yadis + */ +class Services_Yadis_Discovery { + + /** + * @access private + */ + var $DEFAULT_SUFFIX = 'auth'; + + /** + * @access private + */ + var $PREFIX = '_yadis_services_'; + + /** + * Initialize a discovery object. + * + * @param Services_Yadis_PHPSession $session An object which + * implements the Services_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 Services_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(); + $http_response = array(); + + $services = call_user_func($discover_cb, $this->url, + $fetcher); + + $manager = $this->createManager($services, $this->url); + } + + if ($manager) { + $loader = new Services_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. + */ + function cleanup() + { + $manager = $this->getManager(); + if ($manager) { + $service = $manager->current(); + $this->destroyManager(); + } 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 + */ + function &getManager() + { + // 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 Services_Yadis_ManagerLoader(); + $manager = $loader->fromSession(unserialize($manager_str)); + } + + if ($manager && $manager->forURL($this->url)) { + 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 Services_Yadis_ManagerLoader(); + $manager = new Services_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 + */ + function destroyManager() + { + if ($this->getManager() !== null) { + $key = $this->getSessionKey(); + $this->session->del($key); + } + } +} + +?> \ No newline at end of file diff --git a/lib/Services/Yadis/Misc.php b/lib/Services/Yadis/Misc.php new file mode 100644 index 000000000..794b62ead --- /dev/null +++ b/lib/Services/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 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +function Services_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 Services_Yadis_getIPrivateChars() +{ + return array( + array(0xE000, 0xF8FF), + array(0xF0000, 0xFFFFD), + array(0x100000, 0x10FFFD) + ); +} + +function Services_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 Services_Yadis_startswith($s, $stuff) +{ + return strpos($s, $stuff) === 0; +} + +?> \ No newline at end of file diff --git a/lib/Services/Yadis/ParanoidHTTPFetcher.php b/lib/Services/Yadis/ParanoidHTTPFetcher.php new file mode 100644 index 000000000..e14799f83 --- /dev/null +++ b/lib/Services/Yadis/ParanoidHTTPFetcher.php @@ -0,0 +1,177 @@ +<?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 Yadis + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * Interface import + */ +require_once "Services/Yadis/HTTPFetcher.php"; + +/** + * A paranoid {@link Services_Yadis_HTTPFetcher} class which uses CURL + * for fetching. + * + * @package Yadis + */ +class Services_Yadis_ParanoidHTTPFetcher extends Services_Yadis_HTTPFetcher { + function Services_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) + { + $this->data .= $data; + return strlen($data); + } + + function get($url, $extra_headers = null) + { + $stop = time() + $this->timeout; + $off = $this->timeout; + + $redir = true; + + while ($redir && ($off > 0)) { + $this->reset(); + + $c = curl_init(); + if (defined('CURLOPT_NOSIGNAL')) { + curl_setopt($c, CURLOPT_NOSIGNAL, true); + } + + if (!$this->allowedURL($url)) { + trigger_error(sprintf("Fetching URL not allowed: %s", $url), + E_USER_WARNING); + 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); + } + + 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) { + 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 (preg_match("/:/", $header)) { + list($name, $value) = explode(": ", $header, 2); + $new_headers[$name] = $value; + } + } + + return new Services_Yadis_HTTPResponse($url, $code, + $new_headers, $body); + } + + $off = $stop - time(); + } + + trigger_error(sprintf("Timed out fetching: %s", $url), + E_USER_WARNING); + + return null; + } + + function post($url, $body) + { + $this->reset(); + + if (!$this->allowedURL($url)) { + trigger_error(sprintf("Fetching URL not allowed: %s", $url), + E_USER_WARNING); + return null; + } + + $c = curl_init(); + + 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) { + trigger_error("No HTTP code returned", E_USER_WARNING); + return null; + } + + $body = $this->data; + + curl_close($c); + + $new_headers = array(); + + foreach ($this->headers as $header) { + if (preg_match("/:/", $header)) { + list($name, $value) = explode(": ", $header, 2); + $new_headers[$name] = $value; + } + + } + + return new Services_Yadis_HTTPResponse($url, $code, + $new_headers, $body); + } +} + +?> \ No newline at end of file diff --git a/lib/Services/Yadis/ParseHTML.php b/lib/Services/Yadis/ParseHTML.php new file mode 100644 index 000000000..ca8c3644d --- /dev/null +++ b/lib/Services/Yadis/ParseHTML.php @@ -0,0 +1,258 @@ +<?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 Yadis + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * 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 Yadis + */ +class Services_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 Services_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/lib/Services/Yadis/PlainHTTPFetcher.php b/lib/Services/Yadis/PlainHTTPFetcher.php new file mode 100644 index 000000000..6b4b143a6 --- /dev/null +++ b/lib/Services/Yadis/PlainHTTPFetcher.php @@ -0,0 +1,245 @@ +<?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 Yadis + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * Interface import + */ +require_once "Services/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 Yadis + */ +class Services_Yadis_PlainHTTPFetcher extends Services_Yadis_HTTPFetcher { + function get($url, $extra_headers = null) + { + if (!$this->allowedURL($url)) { + trigger_error("Bad URL scheme in url: " . $url, + E_USER_WARNING); + 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 { + trigger_error("fetcher post method doesn't support " . + " scheme '" . $parts['scheme'] . + "', no default port available", + E_USER_WARNING); + return null; + } + } + + $host = $parts['host']; + + if ($parts['scheme'] == 'https') { + $host = 'ssl://' . $host; + } + + $user_agent = "PHP Yadis Library Fetcher"; + + $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 = ""; + while (!feof($sock)) { + $data .= fgets($sock, 1024); + } + + 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)) { + list($name, $value) = explode(": ", $header, 2); + $new_headers[$name] = $value; + } + + } + + return new Services_Yadis_HTTPResponse($url, $code, $new_headers, $body); + } + + function post($url, $body, $extra_headers = null) + { + if (!$this->allowedURL($url)) { + trigger_error("Bad URL scheme in url: " . $url, + E_USER_WARNING); + 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 { + trigger_error("fetcher post method doesn't support scheme '" . + $parts['scheme'] . + "', no default port available", + E_USER_WARNING); + 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) { + trigger_error("Could not connect to " . $parts['host'] . + " port " . $parts['port'], + E_USER_WARNING); + 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 Services_Yadis_HTTPResponse($url, $code, + $headers, $response_body); + } +} + +?> \ No newline at end of file diff --git a/lib/Services/Yadis/XML.php b/lib/Services/Yadis/XML.php new file mode 100644 index 000000000..8e1d23763 --- /dev/null +++ b/lib/Services/Yadis/XML.php @@ -0,0 +1,365 @@ +<?php + +/** + * XML-parsing classes to wrap the domxml and DOM extensions for PHP 4 + * and 5, respectively. + * + * @package Yadis + */ + +/** + * 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 Services_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 Yadis + */ +class Services_Yadis_XMLParser { + /** + * Initialize an instance of Services_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 Services_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 Services_Yadis_XMLParser class for + * details on this class's methods. + * + * @package Yadis + */ +class Services_Yadis_domxml extends Services_Yadis_XMLParser { + function Services_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->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 Services_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 Services_Yadis_XMLParser class for + * details on this class's methods. + * + * @package Yadis + */ +class Services_Yadis_dom extends Services_Yadis_XMLParser { + function Services_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(); + + 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 $__Services_Yadis_defaultParser; +$__Services_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 Services_Yadis_XMLParser $parser An instance of a + * Services_Yadis_XMLParser subclass. + */ +function Services_Yadis_setDefaultParser(&$parser) +{ + global $__Services_Yadis_defaultParser; + $__Services_Yadis_defaultParser =& $parser; +} + +function Services_Yadis_getSupportedExtensions() +{ + return array( + 'dom' => array('classname' => 'Services_Yadis_dom', + 'libname' => array('dom.so', 'dom.dll')), + 'domxml' => array('classname' => 'Services_Yadis_domxml', + 'libname' => array('domxml.so', 'php_domxml.dll')), + ); +} + +/** + * Returns an instance of a Services_Yadis_XMLParser subclass based on + * the availability of PHP extensions for XML parsing. If + * Services_Yadis_setDefaultParser has been called, the parser used in + * that call will be returned instead. + */ +function &Services_Yadis_getXMLParser() +{ + global $__Services_Yadis_defaultParser; + + if (isset($__Services_Yadis_defaultParser)) { + return $__Services_Yadis_defaultParser; + } + + $p = null; + $classname = null; + + $extensions = Services_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 { + Services_Yadis_setDefaultParser($p); + } + + return $p; +} + +?> diff --git a/lib/Services/Yadis/XRDS.php b/lib/Services/Yadis/XRDS.php new file mode 100644 index 000000000..bc82a205f --- /dev/null +++ b/lib/Services/Yadis/XRDS.php @@ -0,0 +1,425 @@ +<?php + +/** + * This module contains the XRDS parsing code. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package Yadis + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * Require the XPath implementation. + */ +require_once 'Services/Yadis/XML.php'; + +/** + * This match mode means a given service must match ALL filters passed + * to the Services_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 Services_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)); + +function Services_Yadis_getNSMap() +{ + return array('xrds' => 'xri://$xrds', + 'xrd' => 'xri://$xrd*($v*2.0)'); +} + +/** + * @access private + */ +function Services_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 + * Services_Yadis_XRDS::services() and + * Services_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 Services_Yadis_Yadis} for more + * information on the role this class plays in Yadis discovery. + * + * @package Yadis + */ +class Services_Yadis_Service { + + /** + * Creates an empty service object. + */ + function Services_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; + } + + /** + * 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 = Services_Yadis_array_scramble($uris[$k]); + $result = array_merge($result, $new_uris); + } + + $result = array_merge($result, + Services_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 Services_Yadis_XMLParser}). + */ + function getElements($name) + { + return $this->parser->evalXPath($name, $this->element); + } +} + +/** + * This class performs parsing of XRDS documents. + * + * You should not instantiate this class directly; rather, call + * parseXRDS statically: + * + * <pre> $xrds = Services_Yadis_XRDS::parseXRDS($xml_string);</pre> + * + * If the XRDS can be parsed and is valid, an instance of + * Services_Yadis_XRDS will be returned. Otherwise, null will be + * returned. This class is used by the Services_Yadis_Yadis::discover + * method. + * + * @package Yadis + */ +class Services_Yadis_XRDS { + + /** + * Instantiate a Services_Yadis_XRDS object. Requires an XPath + * instance which has been used to parse a valid XRDS document. + */ + function Services_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 + * Services_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 Services_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 = Services_Yadis_getXMLParser(); + + $ns_map = Services_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'] != 'xri://$xrd*($v*2.0)') { + return $_null; + } else if (array_key_exists('xmlns', $attrs) && + preg_match('/xri/', $attrs['xmlns']) && + $attrs['xmlns'] != 'xri://$xrd*($v*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 Services_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 Services_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 + * Services_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 + * Services_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/lib/Services/Yadis/XRI.php b/lib/Services/Yadis/XRI.php new file mode 100644 index 000000000..91d385e48 --- /dev/null +++ b/lib/Services/Yadis/XRI.php @@ -0,0 +1,233 @@ +<?php + +/** + * Routines for XRI resolution. + * + * @package Yadis + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +require_once 'Services/Yadis/Misc.php'; +require_once 'Services/Yadis/Yadis.php'; +require_once 'Auth/OpenID.php'; + +function Services_Yadis_getDefaultProxy() +{ + return 'http://proxy.xri.net/'; +} + +function Services_Yadis_getXRIAuthorities() +{ + return array('!', '=', '@', '+', '$', '('); +} + +function Services_Yadis_getEscapeRE() +{ + $parts = array(); + foreach (array_merge(Services_Yadis_getUCSChars(), + Services_Yadis_getIPrivateChars()) as $pair) { + list($m, $n) = $pair; + $parts[] = sprintf("%s-%s", chr($m), chr($n)); + } + + return sprintf('/[%s]/', implode('', $parts)); +} + +function Services_Yadis_getXrefRE() +{ + return '/\((.*?)\)/'; +} + +function Services_Yadis_identifierScheme($identifier) +{ + if (Services_Yadis_startswith($identifier, 'xri://') || + (in_array($identifier[0], Services_Yadis_getXRIAuthorities()))) { + return "XRI"; + } else { + return "URI"; + } +} + +function Services_Yadis_toIRINormal($xri) +{ + if (!Services_Yadis_startswith($xri, 'xri://')) { + $xri = 'xri://' . $xri; + } + + return Services_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 Services_Yadis_escapeForIRI($xri) +{ + $xri = str_replace('%', '%25', $xri); + $xri = preg_replace_callback(Services_Yadis_getXrefRE(), + '_escape_xref', $xri); + return $xri; +} + +function Services_Yadis_toURINormal($xri) +{ + return Services_Yadis_iriToURI(Services_Yadis_toIRINormal($xri)); +} + +function Services_Yadis_iriToURI($iri) +{ + if (1) { + return $iri; + } else { + // According to RFC 3987, section 3.1, "Mapping of IRIs to URIs" + return preg_replace_callback(Services_Yadis_getEscapeRE(), + 'Services_Yadis_pct_escape_unicode', $iri); + } +} + + +function Services_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 Services_Yadis_providerIsAuthoritative($providerID, $canonicalID) +{ + $lastbang = strrpos($canonicalID, '!'); + $p = substr($canonicalID, 0, $lastbang); + return $p == $providerID; +} + +function Services_Yadis_rootAuthority($xri) +{ + // Return the root authority for an XRI. + + $root = null; + + if (Services_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], Services_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 Services_Yadis_XRI($root); +} + +function Services_Yadis_XRI($xri) +{ + if (!Services_Yadis_startswith($xri, 'xri://')) { + $xri = 'xri://' . $xri; + } + return $xri; +} + +function Services_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[count($canonicalID_nodes) - 1]; + $canonicalID = Services_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, '!')); + $parent_list = array(); + + foreach ($parser->evalXPath('xrd:CanonicalID', $xrd) as $c) { + $parent_list[] = Services_Yadis_XRI($parser->content($c)); + } + + if (!in_array($parent_sought, $parent_list)) { + // raise XRDSFraud. + return false; + } + + $childID = $parent_sought; + } + + $root = Services_Yadis_rootAuthority($iname); + if (!Services_Yadis_providerIsAuthoritative($root, $childID)) { + // raise XRDSFraud. + return false; + } + + return $canonicalID; +} + +?> \ No newline at end of file diff --git a/lib/Services/Yadis/XRIRes.php b/lib/Services/Yadis/XRIRes.php new file mode 100644 index 000000000..b87cf0440 --- /dev/null +++ b/lib/Services/Yadis/XRIRes.php @@ -0,0 +1,68 @@ +<?php + +require_once 'Services/Yadis/XRDS.php'; +require_once 'Services/Yadis/XRI.php'; + +class Services_Yadis_ProxyResolver { + function Services_Yadis_ProxyResolver(&$fetcher, $proxy_url = null) + { + $this->fetcher =& $fetcher; + $this->proxy_url = $proxy_url; + if (!$this->proxy_url) { + $this->proxy_url = Services_Yadis_getDefaultProxy(); + } + } + + function queryURL($xri, $service_type = null) + { + // trim off the xri:// prefix + $qxri = substr(Services_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 = Services_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) { + continue; + } + $xrds = Services_Yadis_XRDS::parseXRDS($response->body); + if (!$xrds) { + continue; + } + $canonicalID = Services_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); + } +} + +?> \ No newline at end of file diff --git a/lib/Services/Yadis/Yadis.php b/lib/Services/Yadis/Yadis.php new file mode 100644 index 000000000..338bb3a24 --- /dev/null +++ b/lib/Services/Yadis/Yadis.php @@ -0,0 +1,313 @@ +<?php + +/** + * The core PHP Yadis implementation. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package Yadis + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * Need both fetcher types so we can use the right one based on the + * presence or absence of CURL. + */ +require_once "Services/Yadis/PlainHTTPFetcher.php"; +require_once "Services/Yadis/ParanoidHTTPFetcher.php"; + +/** + * Need this for parsing HTML (looking for META tags). + */ +require_once "Services/Yadis/ParseHTML.php"; + +/** + * Need this to parse the XRDS document during Yadis discovery. + */ +require_once "Services/Yadis/XRDS.php"; + +/** + * 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 "Services/Yadis/Yadis.php";</pre> + * + * To perform Yadis discovery, first call the "discover" method + * statically with a URI parameter: + * + * <pre> $http_response = array(); + * $fetcher = Services_Yadis_Yadis::getHTTPFetcher(); + * $yadis_object = Services_Yadis_Yadis::discover($uri, + * $http_response, $fetcher);</pre> + * + * If the discovery succeeds, $yadis_object will be an instance of + * {@link Services_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 Services_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 Services_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 + * Services_Yadis_Service}. + * + * @package Yadis + */ +class Services_Yadis_Yadis { + + /** + * Returns an HTTP fetcher object. If the CURL extension is + * present, an instance of {@link Services_Yadis_ParanoidHTTPFetcher} + * is returned. If not, an instance of + * {@link Services_Yadis_PlainHTTPFetcher} is returned. + */ + function getHTTPFetcher($timeout = 20) + { + if (Services_Yadis_Yadis::curlPresent()) { + $fetcher = new Services_Yadis_ParanoidHTTPFetcher($timeout); + } else { + $fetcher = new Services_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 + * Services_Yadis_HTTPResponse}. + * + * @param Services_Yadis_HTTPFetcher $fetcher An instance of a + * Services_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 + * Services_Yadis_Yadis, depending on whether the discovery + * succeeded. + */ + function discover($uri, &$http_response, &$fetcher, + $extra_ns_map = null, $timeout = 20) + { + if (!$uri) { + return null; + } + + $request_uri = $uri; + $headers = array("Accept: application/xrds+xml"); + + if (!$fetcher) { + $fetcher = Services_Yadis_Yadis::getHTTPFetcher($timeout); + } + + $response = $fetcher->get($uri, $headers); + $http_response = $response; + + if (!$response) { + return null; + } + + if ($response->status != 200) { + return null; + } + + $xrds_uri = $response->final_url; + $uri = $response->final_url; + $body = $response->body; + + $xrds_header_uri = Services_Yadis_Yadis::_getHeader( + $response->headers, + array('x-xrds-location', + 'x-yadis-location')); + + $content_type = Services_Yadis_Yadis::_getHeader($response->headers, + array('content-type')); + + if ($xrds_header_uri) { + $xrds_uri = $xrds_header_uri; + $response = $fetcher->get($xrds_uri); + $http_response = $response; + if (!$response) { + return null; + } else { + $body = $response->body; + $headers = $response->headers; + $content_type = Services_Yadis_Yadis::_getHeader($headers, + array('content-type')); + } + } + + if (Services_Yadis_Yadis::_getContentType($content_type) != + 'application/xrds+xml') { + // Treat the body as HTML and look for a META tag. + $parser = new Services_Yadis_ParseHTML(); + $new_uri = $parser->getHTTPEquiv($body); + $xrds_uri = null; + if ($new_uri) { + $response = $fetcher->get($new_uri); + if ($response->status != 200) { + return null; + } + $http_response = $response; + $body = $response->body; + $xrds_uri = $new_uri; + $content_type = Services_Yadis_Yadis::_getHeader( + $response->headers, + array('content-type')); + } + } + + $xrds = Services_Yadis_XRDS::parseXRDS($body, $extra_ns_map); + + if ($xrds !== null) { + $y = new Services_Yadis_Yadis(); + + $y->request_uri = $request_uri; + $y->xrds = $xrds; + $y->uri = $uri; + $y->xrds_uri = $xrds_uri; + $y->body = $body; + $y->content_type = $content_type; + + return $y; + } else { + return null; + } + } + + /** + * Instantiates an empty Services_Yadis_Yadis object. This + * constructor should not be used by any user of the library. + * This constructor results in a completely useless object which + * must be populated with valid discovery information. Instead of + * using this constructor, call + * Services_Yadis_Yadis::discover($uri). + */ + function Services_Yadis_Yadis() + { + $this->request_uri = null; + $this->uri = null; + $this->xrds = null; + $this->xrds_uri = null; + $this->body = null; + $this->content_type = null; + } + + /** + * 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 Services_Yadis_Service} + * objects + */ + function services() + { + if ($this->xrds) { + return $this->xrds->services(); + } + + return null; + } +} + +?> \ No newline at end of file diff --git a/templates/default/en/openid-about.php b/templates/default/en/openid-about.php new file mode 100644 index 000000000..f72fc2b88 --- /dev/null +++ b/templates/default/en/openid-about.php @@ -0,0 +1,60 @@ +<?php $this->includeAtTemplateBase('includes/header.php'); ?> + + <div id="header"> + <h1>simpleSAMLphp OpenID</h1> + <div id="poweredby"><img src="/<?php echo $data['baseurlpath']; ?>resources/icons/bino.png" alt="Bino" /></div> + </div> + + <div id="content"> + + <?php if (isset($data['header'])) { echo '<h2>' . $data['header'] . '</h2>'; } ?> + + <p>[ <a href="/<?php echo $data['baseurlpath']; ?>/openid/provider/server.php/sites">List of trusted sites</a> | + About simpleSAMLphp OpenID ]</p> + + + <p>Welcome to the simpleSAMLphp OpenID provider.</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="<?php echo $data['openidserver']; ?>" /> +<link rel="openid.delegation" href="<?php echo $data['openiddelegation']; ?>" /> + + </pre> + + + <p><?php + + if (isset($data['userid'])) { + echo 'You are now logged in as ' . $data['userid']; + } else { + echo '<a href="' . $data['initssourl'] . '">Login</a>'; + } + + ?> + + <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> + + + <h2>About simpleSAMLphp</h2> + <p>Hey! This simpleSAMLphp thing is pretty cool, where can I read more about it? + You can find more information about simpleSAMLphp at <a href="http://rnd.feide.no">the Feide RnD blog</a> over at <a href="http://uninett.no">UNINETT</a>.</p> + +<?php $this->includeAtTemplateBase('includes/footer.php'); ?> + diff --git a/templates/default/en/openid-sites.php b/templates/default/en/openid-sites.php new file mode 100644 index 000000000..024d9e7a8 --- /dev/null +++ b/templates/default/en/openid-sites.php @@ -0,0 +1,81 @@ +<?php $this->includeAtTemplateBase('includes/header.php'); ?> + + <div id="header"> + <h1>simpleSAMLphp OpenID</h1> + <div id="poweredby"><img src="/<?php echo $data['baseurlpath']; ?>resources/icons/bino.png" alt="Bino" /></div> + </div> + + <div id="content"> + + <?php if (isset($data['header'])) { echo '<h2>' . $data['header'] . '</h2>'; } ?> + + + <p>[ List of trusted sites | + <a href="/<?php echo $data['baseurlpath']; ?>/openid/provider/server.php/about">About simpleSAMLphp OpenID</a> ]</p> + + + <p>These decisions have been remembered for this session. All decisions will be forgotten when the session ends.</p> + + + <?php if (isset($data['sites'])) { ?> + + <div class="form"> + <form method="post" action="<?php echo '/' . $data['baseurlpath'] . 'openid/provider/server.php/sites'; ?>"> + <table> + <tbody> + <?php + + $trusted_sites = array(); + $untrusted_sites = array(); + foreach ($data['sites'] as $site => $trusted) { + if ($trusted) { + $trusted_sites[] = $site; + } else { + $untrusted_sites[] = $site; + } + } + + $i = 0; + foreach (array('Trusted Sites' => $trusted_sites, + 'Untrusted Sites' => $untrusted_sites) as + $name => $sites) { + if ($sites) { + echo '<tr><th colspan="2">'. $name . '</th></tr>'; + foreach ($sites as $site) { + $siteid = 'site' . $i; + echo '<tr> + <td><input type="checkbox" name="' . $siteid . '" value="' . + htmlspecialchars($site, ENT_QUOTES) . '" id="' . $siteid . '" /></td> + <td><label for="' . $siteid . '"><code>' . htmlspecialchars($site, ENT_QUOTES) . '</code></label></td> + </tr>'; + $i += 1; + } + } + } + + + ?> + </tbody> + </table> + <input type="submit" name="remove" value="Remove Selected" /> + <input type="submit" name="refresh" value="Refresh List" /> + <input type="submit" name="forget" value="Forget All" /> + </form> + </div> + + <?php } else { ?> + + <p>No sites are remembered for this session. When you authenticate with a site, + you can choose to add it to this list by choosing <q>Remember this decision</q>. + </p> + + <?php } ?> + + + <h2>About simpleSAMLphp</h2> + + <p>Hey! This simpleSAMLphp thing is pretty cool, where can I read more about it? + You can find more information about simpleSAMLphp at <a href="http://rnd.feide.no">the Feide RnD blog</a> over at <a href="http://uninett.no">UNINETT</a>.</p> + +<?php $this->includeAtTemplateBase('includes/footer.php'); ?> + diff --git a/templates/default/en/openid-trust.php b/templates/default/en/openid-trust.php new file mode 100644 index 000000000..2e1266f8d --- /dev/null +++ b/templates/default/en/openid-trust.php @@ -0,0 +1,33 @@ +<?php $this->includeAtTemplateBase('includes/header.php'); ?> + + <div id="header"> + <h1>simpleSAMLphp OpenID</h1> + <div id="poweredby"><img src="/<?php echo $data['baseurlpath']; ?>resources/icons/bino.png" alt="Bino" /></div> + </div> + + <div id="content"> + + <?php if (isset($data['header'])) { echo '<h2>' . $data['header'] . '</h2>'; } ?> + + + <p>[ <a href="/<?php echo $data['baseurlpath']; ?>/openid/provider/server.php/sites">List of trusted sites</a> | + <a href="/<?php echo $data['baseurlpath']; ?>/openid/provider/server.php/about">About simpleSAMLphp OpenID</a> ]</p> + + <div class="form"> + <p>Do you wish to confirm your identity URL (<code><?php echo $data['openidurl']; ?></code>) + with <code><?php echo $data['siteurl']; ?></code>?</p> + <form method="post" action="<?php echo $data['trusturl']; ?>"> + <input type="checkbox" name="remember" value="on" id="remember"><label + for="remember">Remember this decision</label> + <br /> + <input type="submit" name="trust" value="Confirm" /> + <input type="submit" value="Do not confirm" /> + </form> + </div> + + + <h2>About simpleSAMLphp</h2> + <p>Hey! This simpleSAMLphp thing is pretty cool, where can I read more about it? + You can find more information about simpleSAMLphp at <a href="http://rnd.feide.no">the Feide RnD blog</a> over at <a href="http://uninett.no">UNINETT</a>.</p> + +<?php $this->includeAtTemplateBase('includes/footer.php'); ?> \ No newline at end of file diff --git a/www/openid/provider/server.php b/www/openid/provider/server.php new file mode 100644 index 000000000..3508a681a --- /dev/null +++ b/www/openid/provider/server.php @@ -0,0 +1,676 @@ +<?php + + + + + +require_once('../../_include.php'); + +// Include simpleSAMLphp libraries +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/Logger.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XML/AttributeFilter.php'); +require_once('SimpleSAML/XHTML/Template.php'); + +// Include openid libs +require_once 'lib/session.php'; +require_once 'lib/actions.php'; + +require_once "Auth/OpenID.php"; +require_once "Auth/OpenID/Server.php"; +require_once "Auth/OpenID/HMACSHA1.php"; +require_once "Auth/OpenID/FileStore.php"; + +session_start(); + + + + + + + +/* + * CONFIGURATION + */ + + + +/** + * Initialize an OpenID store + * + * @return object $store an instance of OpenID store (see the + * documentation for how to create one) + */ +function getOpenIDStore() +{ + + $config = SimpleSAML_Configuration::getInstance(); + return new Auth_OpenID_FileStore($config->getValue('openid.filestore')); +} + +/** + * Trusted sites is an array of trust roots. + * + * Sites in this list will not have to be approved by the user in + * order to be used. It is OK to leave this value as-is. + * + * In a more robust server, this should be a per-user setting. + */ +$trusted_sites = array( +); + + + + + + +/* + * ACTIONS + */ + + + +/** + * Handle a standard OpenID server request + */ +function action_default() +{ + $server =& getServer(); + $method = $_SERVER['REQUEST_METHOD']; + $request = null; + if ($method == 'GET') { + $request = $_GET; + } else { + $request = $_POST; + } + + $request = Auth_OpenID::fixArgs($request); + $request = $server->decodeRequest($request); + + if (!$request) { + + $config = SimpleSAML_Configuration::getInstance(); + $metadata = new SimpleSAML_XML_MetaDataStore($config); + + $t = new SimpleSAML_XHTML_Template($config, 'openid-about.php'); + $t->data['openidserver'] = $metadata->getGenerated('server', 'openid-provider'); + + + $session = SimpleSAML_Session::getInstance(true); + + $useridfield = $config->getValue('openid.userid_attributename'); + $delegationprefix = $config->getValue('openid.delegation_prefix'); + + $username = 'your_username'; + if (isset($session) && $session->isValid() ) { + $attributes = $session->getAttributes(); + $username = $attributes[$useridfield][0]; + $t->data['userid'] = $username; + } + $idpmeta = $metadata->getMetaDataCurrent('openid-provider'); + + $relaystate = SimpleSAML_Utilities::selfURLNoQuery() . '?RelayState=' . urlencode($_GET['RelayState']) . + '&RequestID=' . urlencode($requestid); + $authurl = SimpleSAML_Utilities::addURLparameter('/' . $config->getValue('baseurlpath') . $idpmeta['auth'], + 'RelayState=' . urlencode($relaystate)); + + $t->data['initssourl'] = $authurl; + $t->data['openiddelegation'] = $delegationprefix . $username; + + + + $t->show(); + exit(0); + } + + setRequestInfo($request); + + if (in_array($request->mode, + array('checkid_immediate', 'checkid_setup'))) { + + if (isTrusted($request->identity, $request->trust_root)) { + $response =& $request->answer(true); + $sreg = getSreg($request->identity); + if (is_array($sreg)) { + foreach ($sreg as $k => $v) { + $response->addField('sreg', $k, $v); + } + } + } else if ($request->immediate) { + $response =& $request->answer(false, getServerURL()); + } else { + if (!getLoggedInUser()) { + // TODO Login + //return login_render(); + check_authenticated_user(); + } + + $config = SimpleSAML_Configuration::getInstance(); + $t = new SimpleSAML_XHTML_Template($config, 'openid-trust.php'); + + $t->data['openidurl'] = getLoggedInUser(); + $t->data['siteurl'] = htmlspecialchars($request->trust_root);; + $t->data['trusturl'] = buildURL('trust', true); + + $t->show(); + exit(0); + + //return trust_render($request); + } + } else { + $response =& $server->handleRequest($request); + } + + $webresponse =& $server->encodeResponse($response); + + 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 (!isset($input['password'])) { + $errors[] = 'Enter a password to continue'; + } + if (count($errors) == 0) { + $openid_url = $input['openid_url']; + $openid_url = Auth_OpenID::normalizeUrl($openid_url); + $password = $input['password']; + if (!checkLogin($openid_url, $password)) { + $errors[] = 'The entered password does not match the ' . + 'entered identity URL.'; + } + } + return array($errors, $openid_url); +} + + + + +function check_authenticated_user() { + + //session_start(); + + $config = SimpleSAML_Configuration::getInstance(); + $metadata = new SimpleSAML_XML_MetaDataStore($config); + $session = SimpleSAML_Session::getInstance(true); + + $logger = new SimpleSAML_Logger(); + + $idpentityid = $metadata->getMetaDataCurrentEntityID('openid-provider'); + $idpmeta = $metadata->getMetaDataCurrent('openid-provider'); + + + /* Check if valid local session exists.. */ + if (!isset($session) || !$session->isValid() ) { + + + + $relaystate = SimpleSAML_Utilities::selfURLNoQuery() . '/login'; + $authurl = SimpleSAML_Utilities::addURLparameter('/' . $config->getValue('baseurlpath') . $idpmeta['auth'], + 'RelayState=' . urlencode($relaystate)); + + + header('Location: ' . $authurl); + exit(0); + } + + $attributes = $session->getAttributes(); + $info = getRequestInfo(); + + + $useridfield = $config->getValue('openid.userid_attributename'); + $delegationprefix = $config->getValue('openid.delegation_prefix'); + + $username = $attributes[$useridfield][0]; + + + $openid_url = $delegationprefix . $username; + + error_log('set logged in user to be [' .$delegationprefix. '][' . $username . ']' ); + setLoggedInUser($openid_url); + +} + + +/** + * Log in a user and potentially continue the requested identity approval + */ +function action_login() +{ + + error_log('action login'); + + //session_start(); + + check_authenticated_user(); + + $info = getRequestInfo(); + + return doAuth($info); + +} + + + + + +/** + * Ask the user whether he wants to trust this site + */ +function action_trust() +{ + $info = getRequestInfo(); + $trusted = isset($_POST['trust']); + if ($info && isset($_POST['remember'])) { + $sites = getSessionSites(); + $sites[$info->trust_root] = $trusted; + setSessionSites($sites); + } + return doAuth($info, $trusted, true); +} + +function action_sites() +{ + $sites = getSessionSites(); + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + if (isset($_POST['forget'])) { + $sites = null; + setSessionSites($sites); + } elseif (isset($_POST['remove'])) { + foreach ($_POST as $k => $v) { + if (preg_match('/^site[0-9]+$/', $k) && isset($sites[$v])) { + unset($sites[$v]); + } + } + setSessionSites($sites); + } + } + + $config = SimpleSAML_Configuration::getInstance(); + $t = new SimpleSAML_XHTML_Template($config, 'openid-sites.php'); + + $t->data['openidurl'] = getLoggedInUser(); + $t->data['sites'] = $sites; + + $t->show(); + exit(0); + + + // TODO Render sites + //return sites_render($sites); +} + + + +/** + * Return an HTTP redirect response + */ +function redirect_render($redir_url) +{ + /* + $headers = array(http_found, + header_content_text, + header_connection_close, + 'Location: ' . $redir_url, + ); + */ + header('Location: ' . $redir_url); + +// $body = sprintf(redirect_message, $redir_url); + // return array($headers, $body); +} + + + + + + + + + + + + + + + + + + + + + + + + + + + +/* + * SESSION + */ + + + +/** + * Get the URL of the current script + */ +function getServerURL() +{ + $path = $_SERVER['SCRIPT_NAME']; + $host = $_SERVER['HTTP_HOST']; + $port = $_SERVER['SERVER_PORT']; + $s = $_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()); + } + return $server; +} + +/** + * Return whether the trust root is currently trusted + */ +function isTrusted($identity_url, $trust_root) +{ + // from config.php + global $trusted_sites; + + if ($identity_url != getLoggedInUser()) { + return false; + } + + if (in_array($trust_root, $trusted_sites)) { + return true; + } + + $sites = getSessionSites(); + return isset($sites[$trust_root]) && $sites[$trust_root]; +} + +/** + * Return a hashed form of the user's password + */ +function hashPassword($password) +{ + return bin2hex(Auth_OpenID_SHA1($password)); +} + +/** + * Check the user's login information + */ +function checkLogin($openid_url, $password) +{ + // from config.php + global $openid_users; + $hash = hashPassword($password); + + return isset($openid_users[$openid_url]) + && $hash == $openid_users[$openid_url]; +} + +/** + * 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 setSessionSites($sites=null) +{ + if (!isset($sites)) { + unset($_SESSION['session_sites']); + } else { + $_SESSION['session_sites'] = serialize($sites); + } +} + +function getSessionSites() +{ + return isset($_SESSION['session_sites']) + ? unserialize($_SESSION['session_sites']) + : false; +} + +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]; + +} + + + + + + + + + + + + + + +/* + * OpenID Transactions + */ + + + +function authCancel($info) +{ + if ($info) { + setRequestInfo(); + $url = $info->getCancelURL(); + } else { + $url = getServerURL(); + } + redirect_render($url); +} + +function doAuth($info, $trusted=null, $fail_cancels=false) +{ + if (!$info) { + // There is no authentication information, so bail + authCancel(null); + } + + $req_url = $info->identity; + $user = getLoggedInUser(); + setRequestInfo($info); + + if ($req_url != $user) { + error_log('simpleSAMLphp doauth():' . 'Your identity ' . $user . + ' does not match the requested identity from the OpenID consumer, which was: ' . $req_url); + $config = SimpleSAML_Configuration::getInstance(); + $t = new SimpleSAML_XHTML_Template($config, 'error.php'); + + $t->data['header'] = 'OpenID identity mismatch'; + $t->data['message'] = 'Your identity ' . $user . ' does not match the requested identity from the + OpenID consumer, which was: ' . $req_url; + $t->data['e'] = new Exception('OpenID Error'); + + $t->show(); + exit(0); + + } + + $sites = getSessionSites(); + $trust_root = $info->trust_root; + $fail_cancels = $fail_cancels || isset($sites[$trust_root]); + $trusted = isset($trusted) ? $trusted : isTrusted($req_url, $trust_root); + + if ($trusted) { + setRequestInfo(); + $server =& getServer(); + $response =& $info->answer(true); + $webresponse =& $server->encodeResponse($response); + + $new_headers = array(); + + foreach ($webresponse->headers as $k => $v) { + $new_headers[] = $k.": ".$v; + } + + + array_walk($new_headers, 'header'); + header(header_connection_close); + print $webresponse->body; + + + } elseif ($fail_cancels) { + authCancel($info); + } else { + + $config = SimpleSAML_Configuration::getInstance(); + $t = new SimpleSAML_XHTML_Template($config, 'openid-trust.php'); + + $t->data['openidurl'] = getLoggedInUser(); + $t->data['siteurl'] = htmlspecialchars($request->trust_root);; + $t->data['trusturl'] = buildURL('trust', true); + + $t->show(); + exit(0); + + + } +} + + + + +/* + * Handle actions + */ + + +//init(); +$action = getAction(); +if (!function_exists($action)) { + $action = 'action_default'; +} +$action(); + + + +?> \ No newline at end of file -- GitLab