diff options
Diffstat (limited to 'Auth')
-rw-r--r-- | Auth/OpenID/Association.php | 284 | ||||
-rw-r--r-- | Auth/OpenID/Consumer/Consumer.php | 973 | ||||
-rw-r--r-- | Auth/OpenID/Consumer/Fetchers.php | 374 | ||||
-rw-r--r-- | Auth/OpenID/Consumer/Parse.php | 288 | ||||
-rw-r--r-- | Auth/OpenID/CryptUtil.php | 855 | ||||
-rw-r--r-- | Auth/OpenID/DiffieHellman.php | 109 | ||||
-rw-r--r-- | Auth/OpenID/HMACSHA1.php | 58 | ||||
-rw-r--r-- | Auth/OpenID/KVForm.php | 102 | ||||
-rw-r--r-- | Auth/OpenID/OIDUtil.php | 283 | ||||
-rw-r--r-- | Auth/OpenID/Store/DumbStore.php | 117 | ||||
-rw-r--r-- | Auth/OpenID/Store/FileStore.php | 652 | ||||
-rw-r--r-- | Auth/OpenID/Store/Interface.php | 179 | ||||
-rw-r--r-- | Auth/OpenID/Store/SQLStore.php | 16 |
13 files changed, 4290 insertions, 0 deletions
diff --git a/Auth/OpenID/Association.php b/Auth/OpenID/Association.php new file mode 100644 index 0000000..638e835 --- /dev/null +++ b/Auth/OpenID/Association.php @@ -0,0 +1,284 @@ +<?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 + */ + +/** + * Includes for utility functions. + */ +require_once('CryptUtil.php'); +require_once('KVForm.php'); +require_once('OIDUtil.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 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. + */ + var $SIG_LENGTH = 20; + + /** + * The ordering and name of keys as stored by serialize. + */ + var $assoc_keys = array( + 'version', + 'handle', + 'secret', + 'issued', + 'lifetime', + 'assoc_type' + ); + + /** + * This is an alternate constructor used by the OpenID consumer + * library to create associations. OpenIDStore implementations + * shouldn't use this constructor. + * + * @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. + */ + 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. + * + * @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 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)); + } + + /** + * This checks to see if two Auth_OpenID_Association instances + * represent different associations. + * + * @return bool $result true if the two instances represent + * different associations, false otherwise. + */ + function not_equal($other) + { + return !($this->equal($other)); + } + + /** + * 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' => Auth_OpenID_toBase64($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::arrayToKV($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::kvToArray($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']; + if ($keys != $class_assoc_keys) { + trigger_error('Unexpected key values: ' . strval($keys), + E_USER_WARNING); + return null; + } + + list($version, $handle, $secret, $issued, $lifetime, $assoc_type) = + $values; + + if ($version != '2') { + trigger_error('Unknown version: ' . $version, E_USER_WARNING); + return null; + } + + $issued = intval($issued); + $lifetime = intval($lifetime); + $secret = Auth_OpenID_fromBase64($secret); + + return new $class_name( + $handle, $secret, $issued, $lifetime, $assoc_type); + } + + /** + * Generate a signature for a sequence of (key, value) pairs + * + * @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) + { + assert($this->assoc_type == 'HMAC-SHA1'); + $kv = Auth_OpenID_KVForm::arrayToKV($pairs); + return Auth_OpenID_hmacSha1($this->secret, $kv); + } + + /** + * Generate a signature for some fields in a dictionary + * + * @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 Auth_OpenID_toBase64($this->sign($pairs)); + } + + function addSignature($fields, &$data, $prefix = 'openid.') + { + $sig = $this->signDict($fields, $data, $prefix); + $signed = implode(",", $fields); + $data[$prefix . 'sig'] = $sig; + $data[$prefix . 'signed'] = $signed; + } + + 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/Auth/OpenID/Consumer/Consumer.php b/Auth/OpenID/Consumer/Consumer.php new file mode 100644 index 0000000..7ac3ab0 --- /dev/null +++ b/Auth/OpenID/Consumer/Consumer.php @@ -0,0 +1,973 @@ +<?php + +/** + * This module documents the main interface with the OpenID consumer + * libary. 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 + * OpenIDConsumer instance. More on the abstract store type and + * concrete implementations of it that are provided in the + * documentation for the constructor of the OpenIDConsumer class. + * + * 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 checks that the entered URL describes an + * OpenID page by fetching it and looking for appropriate link tags + * in the head section. + * 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 a 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 concrete + * implementations that are provided allow the consumer site to store + * the necessary data in several different ways: in the filesystem, in + * a MySQL database, or in an SQLite database. + * + * 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 + * identity 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. + * + * When your site receives that request, it should create an + * Auth_OpenID_Consumer instance, and call beginAuth on it. If + * beginAuth completes successfully, it will return an + * Auth_OpenID_AuthRequest instance. Otherwise it will provide some + * useful information for giving the user an error message. + * + * Now that you have the Auth_OpenID_AuthRequest object, you need to + * preserve the value in its $token field for lookup on the user's + * next request from your site. There are several approaches for + * doing this which will work. If your environment has any kind of + * session-tracking system, storing the token in the session is a good + * approach. If it doesn't you can store the token in either a cookie + * or in the return_to url provided in the next step. + * + * The next step is to call the constructRedirect method on the + * Auth_OpenID_Consumer object. Pass it the Auth_OpenID_AuthRequest + * object returned by the previous call to beginAuth along with the + * return_to and trust_root URLs. The return_to URL 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. + * + * Next, send the user a redirect to the URL generated by + * constructRedirect. + * + * That's the first half of the process. The second half of the + * process is done after the user's ID server sends the user 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 constructRedirect 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. + * + * When handling this request, the first thing to do is check the + * 'openid.return_to' parameter. If it doesn't match the URL that + * the request was actually sent to (the URL the request was actually + * sent to will contain the openid parameters in addition to any in + * the return_to URL, but they should be identical other than that), + * that is clearly suspicious, and the request shouldn't be allowed to + * proceed. + + * Otherwise, the next step is to extract the token value set in the + * first half of the OpenID login. Create a Auth_OpenID_Consumer + * object, and call its completeAuth method with that token and a + * dictionary of all the query arguments. This call will return a + * status code and some additional information describing the the + * server's response. See the documentation for completeAuth for a + * full explanation of the possible responses. + * + * At this point, you have an identity URL that you know belongs to + * the user who made that request. Some sites will use that URL + * directly as the user name. Other sites will want to map that URL + * to a username in the site's traditional namespace. At this point, + * you can take whichever action makes the most sense. + * + * 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/CryptUtil.php"); +require_once("Auth/OpenID/KVForm.php"); +require_once("Auth/OpenID/OIDUtil.php"); +require_once("Auth/OpenID/Association.php"); +require_once("Auth/OpenID/DiffieHellman.php"); +require_once("Auth/OpenID/Consumer/Parse.php"); +require_once("Auth/OpenID/Consumer/Fetchers.php"); + +/** + * This is the status code returned when either the of the beginAuth + * or completeAuth methods return successfully. + */ +$Auth_OpenID_SUCCESS = 'success'; + +/** + * This is the status code completeAuth returns when the value it + * received indicated an invalid login. + */ +$Auth_OpenID_FAILURE = 'failure'; + +/** + * This is the status code completeAuth returns when the + * 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. + */ +$Auth_OpenID_SETUP_NEEDED = 'setup needed'; + +/** + * This is the status code beginAuth returns when it is unable to + * fetch the OpenID URL the user entered. + */ +$Auth_OpenID_HTTP_FAILURE = 'http failure'; + +/** + * 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. +*/ +$Auth_OpenID_PARSE_ERROR = 'parse error'; + +/** + * This is the characters that the nonces are made from. + */ +$_Auth_OpenID_NONCE_CHRS = $GLOBALS['_Auth_OpenID_letters'] . + $GLOBALS['_Auth_OpenID_digits']; + +/** + * This is the number of seconds the tokens generated by this library + * will be valid for. If you want to change the lifetime of a token, + * set this value to the desired lifespan, in seconds. + */ +$_Auth_OpenID_TOKEN_LIFETIME = 60 * 5; // five minutes + +/** + * This is the number of characters in the generated nonce for each + * transaction. + */ +$_Auth_OpenID_NONCE_LEN = 8; + +/** + * This class is the interface to the OpenID consumer logic. + * Instances of it maintain no per-request state, so they can be + * reused (or even used by multiple threads concurrently) as needed. + * + * @package OpenID + */ +class Auth_OpenID_Consumer { + + /** + * This method initializes a new Auth_OpenID_Consumer instance to + * access the library. + * + * @param Auth_OpenID_OpenIDStore $store This must be an object + * that implements the interface in Auth_OpenID_Store. Several + * concrete implementations are provided, to cover most common use + * cases. For stores backed by MySQL, PostgreSQL, or SQLite, see + * the Auth_OpenID_SQLStore class and its sublcasses. For a + * filesystem-backed store, see the Auth_OpenID_FileStore module. + * As a last resort, if it isn't possible for the server to store + * state at all, an instance of Auth_OpenID_DumbStore can be used. + * This should be an absolute last resort, though, as it makes the + * consumer vulnerable to replay attacks over the lifespan of the + * tokens the library creates. + * + * @param Auth_OpenID_HTTPFetcher $fetcher This is an optional + * reference to an instance of Auth_OpenID_HTTPFetcher. If + * present, the provided fetcher is used by the library to fetch + * users' identity pages and make direct requests to the identity + * server. If it is not present, a default fetcher is used. The + * default fetcher uses curl if the Curl bindings are available, + * and uses a raw socket POST if not. + * + * @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_Consumer(&$store, $fetcher = null, $immediate = false) + { + if ($store === null) { + trigger_error("Must supply non-null store to create consumer", + E_USER_ERROR); + return null; + } + + $this->store =& $store; + + if ($fetcher === null) { + $this->fetcher = Auth_OpenID_getHTTPFetcher(); + } else { + $this->fetcher =& $fetcher; + } + + if ($immediate) { + $this->mode = 'checkid_immediate'; + } else { + $this->mode = 'checkid_setup'; + } + + $this->immediate = $immediate; + } + + /** + * This method is called to start the OpenID login process. + * + * First, the user's claimed identity page is fetched, to + * determine their identity server. If the page cannot be fetched + * or if the page does not have the necessary link tags in it, + * this method returns one of $Auth_OpenID_HTTP_FAILURE or + * $Auth_OpenID_PARSE_ERROR, depending on where the process failed. + * + * Second, unless the store provided is a dumb store, it checks to + * see if it has an association with that identity server, and + * creates and stores one if not. + * + * Third, it generates a signed token for this authentication + * transaction, which contains a timestamp, a nonce, and the + * information needed in Step 4 (above) in the module overview. + * The token is used by the library to make handling the various + * pieces of information needed in Step 4 (above) easy and secure. + * + * The token generated must be preserved until Step 4 (above), + * which is after the redirect to the OpenID server takes place. + * This means that the token must be preserved across http + * requests. There are three basic approaches that might be used + * for storing the token. First, the token could be put in the + * return_to URL passed into the constructRedirect method. + * Second, the token could be stored in a cookie. Third, in an + * environment that supports user sessions, the session is a good + * spot to store the token. + * + * @param string $user_url This is the url the user entered as + * their OpenID. This call takes care of normalizing it and + * resolving any redirects the server might issue. + * + * @return array $array This method returns an array containing a + * status code and additional information about the code. + * + * If there was a problem fetching the identity page the user + * gave, the status code is set to $Auth_OpenID_HTTP_FAILURE, and + * the additional information value is either set to null if the + * HTTP transaction failed or the HTTP return code, which will be + * in the 400-500 range. This additional information value may + * change in a future release. + * + * If the identity page fetched successfully, but didn't include + * the correct link tags, the status code is set to + * $Auth_OpenID_PARSE_ERROR, and the additional information value + * is currently set to null. The additional information value may + * change in a future release. + * + * Otherwise, the status code is set to $Auth_OpenID_SUCCESS, and + * the additional information is an instance of + * Auth_OpenID_AuthRequest. The $token attribute contains the + * token to be preserved for the next HTTP request. The + * $server_url might also be of interest, if you wish to blacklist + * or whitelist OpenID servers. The other contents of the object + * are information needed in the constructRedirect call. + */ + function beginAuth($user_url) + { + global $Auth_OpenID_SUCCESS; + + list($status, $info) = $this->_findIdentityInfo($user_url); + if ($status != $Auth_OpenID_SUCCESS) { + return array($status, $info); + } + + list($consumer_id, $server_id, $server_url) = $info; + return $this->_gotIdentityInfo($consumer_id, $server_id, $server_url); + } + + /** + * This method is called to construct the redirect URL sent to the + * browser to ask the server to verify its identity. This is + * called in Step 3 (above) of the flow described in the overview. + * The generated redirect should be sent to the browser which + * initiated the authorization request. + * + * @param Auth_OpenID_AuthRequest $auth_request This must be a + * Auth_OpenID_AuthRequest instance which was returned from a + * previous call to beginAuth. It contains information found + * during the beginAuth call which is needed to build the redirect + * URL. + * + * @param string $return_to This is the URL that will be included + * in the generated redirect as the URL the OpenID server will + * send its response to. The URL passed in must handle OpenID + * authentication responses. + * + * @param string $trust_root This is a URL that will be sent to + * the server to identify this site. The OpenID spec at + * http://www.openid.net/specs.bml#mode-checkid_immediate has more + * information on what the trust_root value is for and what its + * form can be. While the trust root is officially optional in + * the OpenID specification, this implementation requires that it + * be set. Nothing is actually gained by leaving out the trust + * root, as you can get identical behavior by specifying the + * return_to URL as the trust root. + * + * @return string $url This method returns a string containing the + * URL to redirect to when such a URL is successfully constructed. + */ + function constructRedirect($auth_request, $return_to, $trust_root) + { + $assoc = $this->_getAssociation($auth_request->server_url, + $replace = 1); + // Because _getAssociation is asynchronous if the association is + // not already in the store. + + if ($assoc === null) { + trigger_error("Could not get association for redirection", + E_USER_WARNING); + return null; + } + + return $this->_constructRedirect($assoc, $auth_request, + $return_to, $trust_root); + } + + /** + * Given an array of CGI data from PHP, this method replaces + * "openid_" with "openid." in the CGI key strings (NOT the + * values). This is to work around the fact that PHP will mangle + * the CGI key strings to protect against register_globals + * problems. + */ + function fixResponse($arr) + { + // Depending on PHP settings, the query data received may have + // been modified so that incoming "." values in the keys have + // been replaced with underscores. Look specifically for + // "openid_" and replace it with "openid.". + $result = array(); + + foreach ($arr as $key => $value) { + $new_key = str_replace("openid_", "openid.", $key); + $result[$new_key] = $value; + } + + return $result; + } + + /** + * This method is called to interpret the server's response to an + * OpenID request. It is called in Step 4 of the flow described + * in the overview. + * + * The return value is a pair, consisting of a status and + * additional information. The status values are strings, but + * should be referred to by their symbolic values: + * $Auth_OpenID_SUCCESS, $Auth_OpenID_FAILURE, and + * $Auth_OpenID_SETUP_NEEDED. + * + * When $Auth_OpenID_SUCCESS is returned, the additional + * information returned is either null or a string. If it is + * null, it means the user cancelled the login, and no further + * information can be determined. If the additional information + * is a string, it is the identity that has been verified as + * belonging to the user making this request. + * + * When $Auth_OpenID_FAILURE is returned, the additional + * information is either null or a string. In either case, this + * code means that the identity verification failed. If it can be + * determined, the identity that failed to verify is returned. + * Otherwise null is returned. + * + * When $Auth_OpenID_SETUP_NEEDED is returned, the additional + * information is the user setup URL. This is a URL returned only + * as a response to requests made with openid.mode=immediate, + * which indicates that the login was unable to proceed, and the + * user should be sent to that URL if they wish to proceed with + * the login. + * + * @param string $token This is the token for this authentication + * transaction, generated by the call to beginAuth. + * + * @param array $query This is a dictionary-like object containing + * the query parameters the OpenID server included in its redirect + * back to the return_to URL. The keys and values should both be + * url-unescaped. + * + * @return array $array Returns the status of the response and any + * additional information, as described above. + */ + function completeAuth($token, $query) + { + global $Auth_OpenID_SUCCESS, $Auth_OpenID_FAILURE; + + $query = $this->fixResponse($query); + + $mode = Auth_OpenID_array_get($query, 'openid.mode', ''); + + if ($mode == 'cancel') { + return array($Auth_OpenID_SUCCESS, null); + } else if ($mode == 'error') { + + $error = Auth_OpenID_array_get($query, 'openid.error', null); + + if ($error !== null) { + Auth_OpenID_log($error); + } + return array($Auth_OpenID_FAILURE, null); + } else if ($mode == 'id_res') { + return $this->_doIdRes($token, $query); + } else { + return array($Auth_OpenID_FAILURE, null); + } + } + + /** + * @access private + */ + function _gotIdentityInfo($consumer_id, $server_id, $server_url) + { + global $Auth_OpenID_SUCCESS, $_Auth_OpenID_NONCE_CHRS, + $_Auth_OpenID_NONCE_LEN; + + $nonce = Auth_OpenID_CryptUtil::randomString($_Auth_OpenID_NONCE_LEN, + $_Auth_OpenID_NONCE_CHRS); + + $token = $this->_genToken($nonce, $consumer_id, + $server_id, $server_url); + return array($Auth_OpenID_SUCCESS, + new Auth_OpenID_AuthRequest($token, $server_id, + $server_url, $nonce)); + } + + /** + * @access private + */ + function _constructRedirect($assoc, $auth_req, $return_to, $trust_root) + { + $redir_args = array( + 'openid.identity' => $auth_req->server_id, + 'openid.return_to' => $return_to, + 'openid.trust_root' => $trust_root, + 'openid.mode' => $this->mode, + ); + + if ($assoc !== null) { + $redir_args['openid.assoc_handle'] = $assoc->handle; + } + + $this->store->storeNonce($auth_req->nonce); + return strval(Auth_OpenID_appendArgs($auth_req->server_url, + $redir_args)); + } + + /** + * @access private + */ + function _doIdRes($token, $query) + { + global $Auth_OpenID_FAILURE, $Auth_OpenID_SETUP_NEEDED, + $Auth_OpenID_SUCCESS; + + $ret = $this->_splitToken($token); + if ($ret === null) { + return array($Auth_OpenID_FAILURE, null); + } + + list($nonce, $consumer_id, $server_id, $server_url) = $ret; + + $return_to = Auth_OpenID_array_get($query, 'openid.return_to', null); + $server_id2 = Auth_OpenID_array_get($query, 'openid.identity', null); + $assoc_handle = Auth_OpenID_array_get($query, + 'openid.assoc_handle', null); + + if (($return_to === null) || + ($server_id === null) || + ($assoc_handle === null)) { + return array($Auth_OpenID_FAILURE, $consumer_id); + } + + if ($server_id != $server_id2) { + return array($Auth_OpenID_FAILURE, $consumer_id); + } + + $user_setup_url = Auth_OpenID_array_get($query, + 'openid.user_setup_url', null); + + if ($user_setup_url !== null) { + return array($Auth_OpenID_SETUP_NEEDED, $user_setup_url); + } + + $assoc = $this->store->getAssociation($server_url); + + if (($assoc === null) || + ($assoc->handle != $assoc_handle) || + ($assoc->getExpiresIn() <= 0)) { + // It's not an association we know about. Dumb mode is + // our only possible path for recovery. + return array($this->_checkAuth($nonce, $query, $server_url), + $consumer_id); + } + + // Check the signature + $sig = Auth_OpenID_array_get($query, 'openid.sig', null); + $signed = Auth_OpenID_array_get($query, 'openid.signed', null); + if (($sig === null) || + ($signed === null)) { + return array($Auth_OpenID_FAILURE, $consumer_id); + } + + $signed_list = explode(",", $signed); + $v_sig = $assoc->signDict($signed_list, $query); + + if ($v_sig != $sig) { + return array($Auth_OpenID_FAILURE, $consumer_id); + } + + if (!$this->store->useNonce($nonce)) { + return array($Auth_OpenID_FAILURE, $consumer_id); + } + + return array($Auth_OpenID_SUCCESS, $consumer_id); + } + + /** + * @access private + */ + function _checkAuth($nonce, $query, $server_url) + { + global $Auth_OpenID_FAILURE, $Auth_OpenID_SUCCESS; + + // XXX: send only those arguments that were signed? + $signed = Auth_OpenID_array_get($query, 'openid.signed', null); + if ($signed === null) { + return $Auth_OpenID_FAILURE; + } + + $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'; + $post_data = Auth_OpenID_http_build_query($check_args); + + $ret = $this->fetcher->post($server_url, $post_data); + if ($ret === null) { + return $Auth_OpenID_FAILURE; + } + + $results = Auth_OpenID_KVForm::kvToArray($ret[2]); + $is_valid = Auth_OpenID_array_get($results, 'is_valid', 'false'); + + if ($is_valid == 'true') { + $invalidate_handle = Auth_OpenID_array_get($results, + 'invalidate_handle', + null); + if ($invalidate_handle !== null) { + $this->store->removeAssociation($server_url, + $invalidate_handle); + } + + if (!$this->store->useNonce($nonce)) { + return $Auth_OpenID_FAILURE; + } + + return $Auth_OpenID_SUCCESS; + } + + $error = Auth_OpenID_array_get($results, 'error', null); + if ($error !== null) { + Auth_OpenID_log(sprintf("Error message from server during " . + "check_authentication: %s", error)); + } + + return $Auth_OpenID_FAILURE; + } + + /** + * @access private + */ + function _getAssociation($server_url, $replace = false) + { + global $_Auth_OpenID_TOKEN_LIFETIME; + + if ($this->store->isDumb()) { + return null; + } + + $assoc = $this->store->getAssociation($server_url); + + if (($assoc === null) || + ($replace && ($assoc->getExpiresIn() < + $_Auth_OpenID_TOKEN_LIFETIME))) { + $dh = new Auth_OpenID_DiffieHellman(); + $body = $this->_createAssociateRequest($dh); + $assoc = $this->_fetchAssociation($dh, $server_url, $body); + } + + return $assoc; + } + + /** + * @access private + */ + function _genToken($nonce, $consumer_id, $server_id, $server_url) + { + $timestamp = strval(time()); + $elements = array($timestamp, $nonce, + $consumer_id, $server_id, $server_url); + + $joined = implode("\x00", $elements); + $sig = Auth_OpenID_CryptUtil::hmacSha1($this->store->getAuthKey(), + $joined); + + return Auth_OpenID_toBase64($sig . $joined); + } + + /** + * @access private + */ + function _splitToken($token) + { + global $_Auth_OpenID_TOKEN_LIFETIME; + + $token = Auth_OpenID_fromBase64($token); + if (strlen($token) < 20) { + return null; + } + + $sig = substr($token, 0, 20); + $joined = substr($token, 20); + if (Auth_OpenID_CryptUtil::hmacSha1( + $this->store->getAuthKey(), $joined) != $sig) { + return null; + } + + $split = explode("\x00", $joined); + if (count($split) != 5) { + return null; + } + + $ts = intval($split[0]); + if ($ts == 0) { + return null; + } + + if ($ts + $_Auth_OpenID_TOKEN_LIFETIME < time()) { + return null; + } + + return array_slice($split, 1); + } + + /** + * @access private + */ + function _findIdentityInfo($identity_url) + { + global $Auth_OpenID_HTTP_FAILURE; + + $url = Auth_OpenID_normalizeUrl($identity_url); + $ret = $this->fetcher->get($url); + if ($ret === null) { + return array($Auth_OpenID_HTTP_FAILURE, null); + } + + list($http_code, $consumer_id, $data) = $ret; + if ($http_code != 200) { + return array($Auth_OpenID_HTTP_FAILURE, $http_code); + } + + // This method is split in two this way to allow for + // asynchronous implementations of _findIdentityInfo. + return $this->_parseIdentityInfo($data, $consumer_id); + } + + /** + * @access private + */ + function _parseIdentityInfo($data, $consumer_id) + { + global $Auth_OpenID_PARSE_ERROR, $Auth_OpenID_SUCCESS; + + $link_attrs = Auth_OpenID_parseLinkAttrs($data); + $server = Auth_OpenID_findFirstHref($link_attrs, 'openid.server'); + $delegate = Auth_OpenID_findFirstHref($link_attrs, 'openid.delegate'); + + if ($server === null) { + return array($Auth_OpenID_PARSE_ERROR, null); + } + + if ($delegate !== null) { + $server_id = $delegate; + } else { + $server_id = $consumer_id; + } + + $urls = array($consumer_id, $server_id, $server); + + $normalized = array(); + + foreach ($urls as $url) { + $normalized[] = Auth_OpenID_normalizeUrl($url); + } + + return array($Auth_OpenID_SUCCESS, $normalized); + } + + /** + * @access private + */ + function _createAssociateRequest($dh, $args = null) + { + global $_Auth_OpenID_DEFAULT_MOD, $_Auth_OpenID_DEFAULT_GEN; + + if ($args === null) { + $args = array(); + } + + $cpub = Auth_OpenID_CryptUtil::longToBase64($dh->public); + + $args = array_merge($args, array( + 'openid.mode' => 'associate', + 'openid.assoc_type' => 'HMAC-SHA1', + 'openid.session_type' => 'DH-SHA1', + 'openid.dh_consumer_public' => $cpub + )); + + if (($dh->mod != $_Auth_OpenID_DEFAULT_MOD) || + ($dh->gen != $_Auth_OpenID_DEFAULT_GEN)) { + $args = array_merge($args, + array( + 'openid.dh_modulus' => + Auth_OpenID_CryptUtil::longToBase64($dh->modulus), + 'openid.dh_gen' => + Auth_OpenID_CryptUtil::longToBase64($dh->generator) + )); + } + + return Auth_OpenID_http_build_query($args); + } + + /** + * @access private + */ + function _fetchAssociation($dh, $server_url, $body) + { + $ret = $this->fetcher->post($server_url, $body); + if ($ret === null) { + $fmt = 'Getting association: failed to fetch URL: %s'; + Auth_OpenID_log(sprintf($fmt, $server_url)); + return null; + } + + list($http_code, $url, $data) = $ret; + $results = Auth_OpenID_KVForm::kvToArray($data); + if ($http_code == 400) { + $server_error = Auth_OpenID_array_get($results, 'error', + '<no message from server>'); + + $fmt = 'Getting association: error returned from server %s: %s'; + Auth_OpenID_log(sprintf($fmt, $server_url, $server_error)); + return null; + } else if ($http_code != 200) { + $fmt = 'Getting association: bad status code from server %s: %s'; + Auth_OpenID_log(sprintf($fmt, $server_url, $http_code)); + return null; + } + + $results = Auth_OpenID_KVForm::kvToArray($data); + + return $this->_parseAssociation($results, $dh, $server_url); + } + + /** + * @access private + */ + function _parseAssociation($results, $dh, $server_url) + { + $required_keys = array('assoc_type', 'assoc_handle', + 'dh_server_public', 'enc_mac_key'); + + foreach ($required_keys as $key) { + if (!array_key_exists($key, $results)) { + Auth_OpenID_log(sprintf("Getting association: missing key in ". + "response from %s: %s", + $server_url, $key), + E_USER_WARNING); + return null; + } + } + + $assoc_type = $results['assoc_type']; + if ($assoc_type != 'HMAC-SHA1') { + $fmt = 'Unsupported assoc_type returned from server %s: %s'; + Auth_OpenID_log(sprintf($fmt, $server_url, $assoc_type)); + return null; + } + + $assoc_handle = $results['assoc_handle']; + $expires_in = intval(Auth_OpenID_array_get($results, 'expires_in', + '0')); + + $session_type = Auth_OpenID_array_get($results, 'session_type', null); + if ($session_type === null) { + $secret = Auth_OpenID_fromBase64($results['mac_key']); + } else { + $fmt = 'Unsupported session_type returned from server %s: %s'; + if ($session_type != 'DH-SHA1') { + Auth_OpenID_log(sprintf($fmt, $server_url, $session_type)); + return null; + } + + $spub = Auth_OpenID_CryptUtil::base64ToLong( + $results['dh_server_public']); + + $enc_mac_key = Auth_OpenID_CryptUtil::fromBase64( + $results['enc_mac_key']); + + $secret = $dh->xorSecret($spub, $enc_mac_key); + } + + $assoc = Auth_OpenID_Association::fromExpiresIn($expires_in, + $assoc_handle, + $secret, + $assoc_type); + + $this->store->storeAssociation($server_url, $assoc); + return $assoc; + } +} + +/** + * This class represents an in-progress OpenID authentication request. + * It exists to make transferring information between the beginAuth + * and constructRedirect methods easier. Users of the OpenID consumer + * library will need to be aware of the $token value, and may care + * about the $server_url value. All other fields are internal + * information for the library which the user of the library shouldn't + * touch at all. + * + * The 'token' is the token generated by the library. It must be + * saved until the user's return request, via whatever mechanism works + * best for this consumer application. + * + * The 'server_url' is the URL of the identity server that will be + * used. It isn't necessary to do anything with this value, but it is + * available for consumers that wish to either blacklist or whitelist + * OpenID servers. + * + * @package OpenID + */ +class Auth_OpenID_AuthRequest { + function Auth_OpenID_AuthRequest($token, $server_id, $server_url, $nonce) + { + $this->token = $token; + $this->server_id = $server_id; + $this->server_url = $server_url; + $this->nonce = $nonce; + } +} + +?> diff --git a/Auth/OpenID/Consumer/Fetchers.php b/Auth/OpenID/Consumer/Fetchers.php new file mode 100644 index 0000000..6918fc5 --- /dev/null +++ b/Auth/OpenID/Consumer/Fetchers.php @@ -0,0 +1,374 @@ +<?php + +/** + * This module contains the HTTP fetcher interface and several + * 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 + */ + +/** + * Specify a socket timeout setting, in seconds. + */ +$_Auth_OpenID_socket_timeout = 20; + +/** + * Specify allowed URL schemes for fetching. + */ +$_Auth_OpenID_allowed_schemes = array('http', 'https'); + +/** + * This class is the interface for HTTP fetchers the OpenID consumer + * library uses. This interface is only important if you need to + * write a new fetcher for some reason. + * + * @package OpenID + */ +class Auth_OpenID_HTTPFetcher { + + /** + * This performs an HTTP get, following redirects along the way. + * + * @return array $tuple This returns a three-tuple on success. + * The first value is the http return code. The second value is + * the final url that was fetched, after following any redirects. + * The third value is the data that was retrieved from the site. + * If the fetch didn't succeed, return null. + */ + function get($url) + { + trigger_error("not implemented", E_USER_ERROR); + } + + /** + * This performs an HTTP post. If it makes sense, it will follow + * redirects along the way. + * + * @return array $tuple This returns a three-tuple on success. + * The first value is the http return code. The second value is + * the final url that was fetched, after following any redirects. + * The third value is the data that was retrieved from the site. + * If the fetch didn't succeed, return null. + */ + function post($url, $body) + { + trigger_error("not implemented", E_USER_ERROR); + } +} + +/** + * Detect the presence of Curl and set a flag accordingly. + */ +$_Auth_OpenID_curl_found = false; +if (function_exists('curl_init')) { + $_Auth_OpenID_curl_found = true; +} + +function Auth_OpenID_getHTTPFetcher() +{ + global $_Auth_OpenID_curl_found; + if (!$_Auth_OpenID_curl_found) { + $fetcher = new Auth_OpenID_PlainHTTPFetcher(); + } else { + $fetcher = new Auth_OpenID_ParanoidHTTPFetcher(); + } + + return $fetcher; +} + +function Auth_OpenID_allowedURL($url) +{ + global $_Auth_OpenID_allowed_schemes; + foreach ($_Auth_OpenID_allowed_schemes as $scheme) { + if (strpos($url, sprintf("%s://", $scheme)) == 0) { + return true; + } + } + + return false; +} + +/** + * This class implements a plain, hand-built socket-based fetcher + * which will be used in the event that CURL is unavailable. + * + * @package OpenID + */ +class Auth_OpenID_PlainHTTPFetcher extends Auth_OpenID_HTTPFetcher { + /** + * @access private + */ + function _fetch($url) + { + $data = @file_get_contents($url); + + if ($data !== false) { + return array(200, $url, $data); + } else { + return null; + } + } + + function get($url) + { + if (!Auth_OpenID_allowedURL($url)) { + trigger_error("Bad URL scheme in url: " . $url, + E_USER_WARNING); + return null; + } + + return $this->_fetch($url); + } + + function post($url, $body) + { + global $_Auth_OpenID_socket_timeout; + + if (!Auth_OpenID_allowedURL($url)) { + trigger_error("Bad URL scheme in url: " . $url, + E_USER_WARNING); + return null; + } + + $parts = parse_url($url); + + $headers = array(); + + $headers[] = "POST ".$parts['path']." HTTP/1.1"; + $headers[] = "Host: " . $parts['host']; + $headers[] = "Content-type: application/x-www-form-urlencoded"; + $headers[] = "Content-length: " . strval(strlen($body)); + + // 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; + } + } + + // Connect to the remote server. + $sock = fsockopen($parts['host'], $parts['port']); + stream_set_timeout($sock, $_Auth_OpenID_socket_timeout); + + if ($sock === false) { + trigger_error("Could not connect to " . $parts['host'] . + " port " . $parts['port'], + E_USER_WARNING); + return null; + } + + // 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]; + + return array($code, $url, $response_body); + } +} + +/** + * An array to store headers and data from Curl calls. + */ +$_Auth_OpenID_curl_data = array(); + +/** + * A function to prepare a "slot" in the global $_Auth_OpenID_curl_data + * array so curl data can be stored there by curl callbacks in the + * paranoid fetcher. + */ +function _initResponseSlot($ch) +{ + global $_Auth_OpenID_curl_data; + $key = strval($ch); + if (!array_key_exists($key, $_Auth_OpenID_curl_data)) { + $_Auth_OpenID_curl_data[$key] = array('headers' => array(), + 'body' => ""); + } + return $key; +} + +/** + * A callback function for curl so headers can be stored. + */ +function _writeHeaders($ch, $data) +{ + global $_Auth_OpenID_curl_data; + $key = _initResponseSlot($ch); + $_Auth_OpenID_curl_data[$key]['headers'][] = rtrim($data); + return strlen($data); +} + +/** + * A callback function for curl so page data can be stored. + */ +function _writeData($ch, $data) +{ + global $_Auth_OpenID_curl_data; + $key = _initResponseSlot($ch); + $_Auth_OpenID_curl_data[$key]['body'] .= $data; + return strlen($data); +} + + +/** + * A paranoid Auth_OpenID_HTTPFetcher class which uses CURL for + * fetching. + * + * @package OpenID + */ +class Auth_OpenID_ParanoidHTTPFetcher extends Auth_OpenID_HTTPFetcher { + function Auth_OpenID_ParanoidHTTPFetcher() + { + global $_Auth_OpenID_curl_found; + if (!$_Auth_OpenID_curl_found) { + trigger_error("Cannot use this class; CURL extension not found", + E_USER_ERROR); + } + } + + /** + * @access private + */ + function _findRedirect($headers) + { + foreach ($headers as $line) { + if (strpos($line, "Location: ") == 0) { + $parts = explode(" ", $line, 2); + return $parts[1]; + } + } + return null; + } + + function get($url) + { + global $_Auth_OpenID_socket_timeout; + global $_Auth_OpenID_curl_data; + + $c = curl_init(); + + $curl_key = _initResponseSlot($c); + + curl_setopt($c, CURLOPT_NOSIGNAL, true); + + $stop = time() + $_Auth_OpenID_socket_timeout; + $off = $_Auth_OpenID_socket_timeout; + + while ($off > 0) { + if (!Auth_OpenID_allowedURL($url)) { + trigger_error(sprintf("Fetching URL not allowed: %s", $url), + E_USER_WARNING); + return null; + } + + curl_setopt($c, CURLOPT_WRITEFUNCTION, "_writeData"); + curl_setopt($c, CURLOPT_HEADERFUNCTION, "_writeHeaders"); + curl_setopt($c, CURLOPT_TIMEOUT, $off); + curl_setopt($c, CURLOPT_URL, $url); + + curl_exec($c); + + $code = curl_getinfo($c, CURLINFO_HTTP_CODE); + $body = $_Auth_OpenID_curl_data[$curl_key]['body']; + $headers = $_Auth_OpenID_curl_data[$curl_key]['headers']; + + if (!$code) { + trigger_error("No HTTP code returned", E_USER_WARNING); + return null; + } + + if (in_array($code, array(301, 302, 303, 307))) { + $url = $this->_findRedirect($headers); + } else { + curl_close($c); + return array($code, $url, $body); + } + + $off = $stop - time(); + } + + trigger_error(sprintf("Timed out fetching: %s", $url), + E_USER_WARNING); + + return null; + } + + function post($url, $body) + { + global $_Auth_OpenID_socket_timeout; + global $_Auth_OpenID_curl_data; + + if (!Auth_OpenID_allowedURL($url)) { + trigger_error(sprintf("Fetching URL not allowed: %s", $url), + E_USER_WARNING); + return null; + } + + $c = curl_init(); + + $curl_key = _initResponseSlot($c); + + curl_setopt($c, CURLOPT_NOSIGNAL, true); + curl_setopt($c, CURLOPT_POST, true); + curl_setopt($c, CURLOPT_POSTFIELDS, $body); + curl_setopt($c, CURLOPT_TIMEOUT, $_Auth_OpenID_socket_timeout); + curl_setopt($c, CURLOPT_URL, $url); + curl_setopt($c, CURLOPT_WRITEFUNCTION, "_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 = $_Auth_OpenID_curl_data[$curl_key]['body']; + + curl_close($c); + return array($code, $url, $body); + } +} + +?>
\ No newline at end of file diff --git a/Auth/OpenID/Consumer/Parse.php b/Auth/OpenID/Consumer/Parse.php new file mode 100644 index 0000000..69f386f --- /dev/null +++ b/Auth/OpenID/Consumer/Parse.php @@ -0,0 +1,288 @@ +<?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. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * Specify some flags for use with regex matching. + */ +$_Auth_OpenID_re_flags = "si"; + +/** + * Stuff to remove before we start looking for tags + */ +$_Auth_OpenID_removed_re = "<!--.*?-->|" . + "<!\[CDATA\[.*?\]\]>|" . + "<script\b(?!:)[^>]*>.*?<\/script>"; + +/** + * Starts with the tag name at a word boundary, where the tag name is + * not a namespace + */ +$_Auth_OpenID_tag_expr = "<%s\b(?!:)([^>]*?)" . + "(?:\/>|>(.*?)" . + "(?:<\/?%s\s*>|\Z))"; + +/** + * Returns a regular expression that will match a given tag in an SGML + * string. + */ +function Auth_OpenID_tagMatcher($tag_name, $close_tags = null) +{ + global $_Auth_OpenID_tag_expr, $_Auth_OpenID_re_flags; + + if ($close_tags) { + $options = implode("|", array_merge(array($tag_name), $close_tags)); + $closer = sprintf("(?:%s)", $options); + } else { + $closer = $tag_name; + } + + $expr = sprintf($_Auth_OpenID_tag_expr, $tag_name, $closer); + return sprintf("/%s/%s", $expr, $_Auth_OpenID_re_flags); +} + +function Auth_OpenID_html_find() +{ + return Auth_OpenID_tagMatcher('html'); +} + +function Auth_OpenID_head_find() +{ + return Auth_OpenID_tagMatcher('head', array('body')); +} + +$_Auth_OpenID_attr_find = '\b(\w+)=("[^"]*"|\'[^\']*\'|[^\'"\s\/<>]+)'; + +$_Auth_OpenID_link_find = sprintf("/<link\b(?!:)([^>]*)(?!<)>/%s", + $_Auth_OpenID_re_flags); + +$_Auth_OpenID_entity_replacements = array( + 'amp' => '&', + 'lt' => '<', + 'gt' => '>', + 'quot' => '"' + ); + +$_Auth_OpenID_attr_find = sprintf("/%s/%s", + $_Auth_OpenID_attr_find, + $_Auth_OpenID_re_flags); + +$_Auth_OpenID_removed_re = sprintf("/%s/%s", + $_Auth_OpenID_removed_re, + $_Auth_OpenID_re_flags); + +$_Auth_OpenID_ent_replace = + sprintf("&(%s);", implode("|", + $_Auth_OpenID_entity_replacements)); + +function Auth_OpenID_replace_entities($str) +{ + global $_Auth_OpenID_entity_replacements; + foreach ($_Auth_OpenID_entity_replacements as $old => $new) { + $str = preg_replace(sprintf("/&%s;/", $old), $new, $str); + } + return $str; +} + +function Auth_OpenID_remove_quotes($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 Auth_OpenID_parseLinkAttrs($html) +{ + + global $_Auth_OpenID_removed_re, + $_Auth_OpenID_link_find, + $_Auth_OpenID_attr_find; + + $stripped = preg_replace($_Auth_OpenID_removed_re, + "", + $html); + + // Try to find the <HTML> tag. + $html_re = Auth_OpenID_html_find(); + $html_matches = array(); + if (!preg_match($html_re, $stripped, $html_matches)) { + return array(); + } + + // Try to find the <HEAD> tag. + $head_re = Auth_OpenID_head_find(); + $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($_Auth_OpenID_link_find, $head_matches[0], + $link_matches)) { + return array(); + } + + foreach ($link_matches[0] as $link) { + $attr_matches = array(); + preg_match_all($_Auth_OpenID_attr_find, $link, $attr_matches); + $link_attrs = array(); + foreach ($attr_matches[0] as $index => $full_match) { + $name = $attr_matches[1][$index]; + $value = Auth_OpenID_replace_entities( + Auth_OpenID_remove_quotes( + $attr_matches[2][$index])); + + $link_attrs[$name] = $value; + } + $link_data[] = $link_attrs; + } + + return $link_data; +} + +function Auth_OpenID_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 Auth_OpenID_linkHasRel($link_attrs, $target_rel) +{ + // Does this link have target_rel as a relationship? + // XXX: TESTME + $rel_attr = Auth_OpenID_array_get($link_attrs, 'rel', null); + return ($rel_attr && Auth_OpenID_relMatches($rel_attr, $target_rel)); +} + +function Auth_OpenID_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 (Auth_OpenID_linkHasRel($attr, $target_rel)) { + $result[] = $attr; + } + } + + return $result; +} + +function Auth_OpenID_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 = Auth_OpenID_findLinksRel($link_attrs_list, $target_rel); + if (!$matches) { + return null; + } + $first = $matches[0]; + return Auth_OpenID_array_get($first, 'href', null); +} + +?>
\ No newline at end of file diff --git a/Auth/OpenID/CryptUtil.php b/Auth/OpenID/CryptUtil.php new file mode 100644 index 0000000..a20a5ed --- /dev/null +++ b/Auth/OpenID/CryptUtil.php @@ -0,0 +1,855 @@ +<?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. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * Require the HMAC/SHA-1 implementation for creating such hashes. + */ +require_once('HMACSHA1.php'); + +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'); +} + +/** + * Auth_OpenID_CryptUtil houses static utility functions. + * + * @package OpenID + * @static + */ +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_USE_INSECURE_RAND</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) + { + $bytes = ''; + $f = @fopen(Auth_OpenID_RAND_SOURCE, "r"); + if ($f === false) { + if (!defined('Auth_OpenID_USE_INSECURE_RAND')) { + trigger_error('Set Auth_OpenID_USE_INSECURE_RAND to ' . + 'continue with insecure random.', + E_USER_ERROR); + } + $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); + fclose($f); + } + return $bytes; + } + + /** + * Computes the maximum integer value for this PHP installation. + * + * @return int $max_int_value The maximum integer value for this + * PHP installation + */ + function maxint() + { + /** + * quick-and-dirty function for PHP int size -- assumes + * largest integer is of form 2^n - 1 + */ + $to_test = pow(2, 16); + while (1) { + $last = $to_test; + $to_test = 2 * $to_test; + if (($to_test < $last) || (!is_int($to_test))) { + return($last + ($last - 1)); + } + } + } + + /** + * Computes the SHA1 hash. + * + * @param string $str The input string. + * @return string The resulting SHA1 hash, in binary form. + */ + function sha1($str) + { + return Auth_OpenID_sha1_raw($str); + } + + /** + * Computes an HMAC-SHA1 digest. + * + * @param string $key The key used to generate the HMAC-SHA1 digest + * @param string $text The text to be hashed + * @return string $digest The raw HMAC-SHA1 digest + */ + function hmacSha1($key, $text) + { + return Auth_OpenID_HMACSHA1($key, $text); + } + + /** + * Converts a base64-encoded string to its raw binary equivalent. + * + * @param string $str The base64-encoded string to decode + * @return string $raw The decoded binary data + */ + function fromBase64($str) + { + return base64_decode($str); + } + + /** + * Converts a raw binary string to its base64-encoded equivalent. + * + * @param string $str The raw binary data to encode + * @return string $raw The base64-encoded version of $str + */ + function toBase64($str) + { + return base64_encode($str); + } + + /** + * 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) + { + + $lib =& Auth_OpenID_MathLibrary::getLibWrapper(); + + $cmp = $lib->cmp($long, 0); + if ($cmp < 0) { + print "longToBytes takes only positive integers."; + return null; + } + + if ($cmp == 0) { + return "\x00"; + } + + $bytes = array(); + + while ($lib->cmp($long, 0) > 0) { + array_unshift($bytes, $lib->mod($long, 256)); + $long = $lib->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 long integer, returns the number converted to a binary + * string. This function accepts "long" numbers within the PHP + * integer range (usually 32 bits). + * + * @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_platform($long) + { + + if ($long < 0) { + print "longToBytes_platform takes only positive integers."; + return null; + } + + return pack('N', $long); + } + + /** + * 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) + { + $lib =& Auth_OpenID_MathLibrary::getLibWrapper(); + + 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 = $lib->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 = $lib->mul($n, pow(2, 8)); + $n = $lib->add($n, $byte); + } + + return $n; + } + + /** + * 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_platform($str) + { + if ($str === null) { + return null; + } + + $data = unpack('Nx', $str); + return $data['x']; + } + + /** + * Converts a base64-encoded string to a long number. + * + * @param string $str A base64-encoded string + * @return integer $long A long number + */ + function base64ToLong($str) + { + return Auth_OpenID_CryptUtil::binaryToLong( + Auth_OpenID_CryptUtil::fromBase64($str)); + } + + /** + * Converts a long number to its base64-encoded representation. + * + * @param integer $long The long number to be converted + * @return string $str The base64-encoded version of $long + */ + function longToBase64($long) + { + return Auth_OpenID_CryptUtil::toBase64( + Auth_OpenID_CryptUtil::longToBinary($long)); + } + + /** + * Given two strings of equal length, computes the exclusive-OR of + * the two strings' ordinal values and returns the resulting + * string. + * + * @param string $x A string + * @param string $y A string + * @return string $result The result of $x XOR $y + */ + function strxor($x, $y) + { + if (strlen($x) != strlen($y)) { + return null; + } + + $str = ""; + for ($i = 0; $i < strlen($x); $i++) { + $str .= chr(ord($x[$i]) ^ ord($y[$i])); + } + + return $str; + } + + /** + * Reverses a string or array. + * + * @param mixed $list A string or an array + * @return mixed $result The reversed string or array + */ + function reversed($list) + { + if (is_string($list)) { + return strrev($list); + } else if (is_array($list)) { + return array_reverse($list); + } else { + return null; + } + } + + /** + * 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 randrange($start, $stop = null, $step = 1) + { + + static $Auth_OpenID_CryptUtil_duplicate_cache = array(); + $lib =& Auth_OpenID_MathLibrary::getLibWrapper(); + + if ($stop == null) { + $stop = $start; + $start = 0; + } + + $r = $lib->div($lib->sub($stop, $start), $step); + + // DO NOT MODIFY THIS VALUE. + $rbytes = Auth_OpenID_CryptUtil::longToBinary($r); + + if (array_key_exists($rbytes, $Auth_OpenID_CryptUtil_duplicate_cache)) { + list($duplicate, $nbytes) = + $Auth_OpenID_CryptUtil_duplicate_cache[$rbytes]; + } else { + if ($rbytes[0] == "\x00") { + $nbytes = strlen($rbytes) - 1; + } else { + $nbytes = strlen($rbytes); + } + + $mxrand = $lib->pow(256, $nbytes); + + // If we get a number less than this, then it is in the + // duplicated range. + $duplicate = $lib->mod($mxrand, $r); + + if (count($Auth_OpenID_CryptUtil_duplicate_cache) > 10) { + $Auth_OpenID_CryptUtil_duplicate_cache = array(); + } + + $Auth_OpenID_CryptUtil_duplicate_cache[$rbytes] = + array($duplicate, $nbytes); + } + + while (1) { + $bytes = "\x00" . Auth_OpenID_CryptUtil::getBytes($nbytes); + $n = Auth_OpenID_CryptUtil::binaryToLong($bytes); + // Keep looping if this value is in the low duplicated + // range + if ($lib->cmp($n, $duplicate) >= 0) { + break; + } + } + + return $lib->add($start, $lib->mul($lib->mod($n, $r), $step)); + } + + /** + * Returns a random number in the specified range. This function + * accepts $start, $stop, and $step values within the platform + * integer range. + * + * @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 randrange_platform($start, $stop = null, $step = 1) + { + + static $Auth_OpenID_CryptUtil_duplicate_cache = array(); + + if ($stop == null) { + $stop = $start; + $start = 0; + } + + $r = ($stop - $start) / $step; + + // DO NOT MODIFY THIS VALUE. + $rbytes = Auth_OpenID_CryptUtil::longToBinary_platform($r); + + if (array_key_exists($rbytes, $Auth_OpenID_CryptUtil_duplicate_cache)) { + list($duplicate, $nbytes) = + $Auth_OpenID_CryptUtil_duplicate_cache[$rbytes]; + } else { + if ($rbytes[0] == "\x00") { + $nbytes = strlen($rbytes) - 1; + } else { + $nbytes = strlen($rbytes); + } + + $mxrand = pow(256, $nbytes); + + // If we get a number less than this, then it is in the + // duplicated range. + $duplicate = $mxrand % $r; + + if (count($Auth_OpenID_CryptUtil_duplicate_cache) > 10) { + $Auth_OpenID_CryptUtil_duplicate_cache = array(); + } + + $Auth_OpenID_CryptUtil_duplicate_cache[$rbytes] = + array($duplicate, $nbytes); + } + + while (1) { + $bytes = "\x00" . Auth_OpenID_CryptUtil::getBytes($nbytes); + $n = Auth_OpenID_CryptUtil::binaryToLong_platform($bytes); + // Keep looping if this value is in the low duplicated + // range + if ($n >= $duplicate) { + break; + } + } + + return $start + ($n % $r) * $step; + } + + /** + * 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, $chrs = null) + { + if ($chrs === null) { + return Auth_OpenID_CryptUtil::getBytes($length); + } else { + $n = strlen($chrs); + $str = ""; + for ($i = 0; $i < $length; $i++) { + $str .= $chrs[Auth_OpenID_CryptUtil::randrange_platform($n)]; + } + return $str; + } + } +} + +/** + * Exposes math library functionality. + * + * Auth_OpenID_MathWrapper is a base class that defines the interface + * to a math library like GMP or BCmath. This library will attempt to + * use an available long number implementation. If a library like GMP + * is found, the appropriate Auth_OpenID_MathWrapper subclass will be + * instantiated and used for mathematics operations on large numbers. + * This base class wraps only native PHP functionality. See + * Auth_OpenID_MathWrapper subclasses for access to particular long + * number implementations. + * + * @package OpenID + */ +class Auth_OpenID_MathWrapper { + /** + * The type of the Auth_OpenID_MathWrapper class. This value + * describes the library or module being wrapped. Users of + * Auth_OpenID_MathWrapper instances should check this value if + * they care about the type of math functionality being exposed. + */ + var $type = 'dumb'; + + /** + * Returns a random number in the specified range. + */ + function random($min, $max) + { + return mt_rand($min, $max); + } + + /** + * Returns $base raised to the $exponent power. + */ + function pow($base, $exponent) + { + return pow($base, $exponent); + } + + /** + * Returns the sum of $x and $y. + */ + function add($x, $y) + { + return $x + $y; + } + + /** + * Returns -1 if $x < $y, 0 if $x == $y, and 1 if $x > $y. + */ + function cmp($x, $y) + { + if ($x > $y) { + return 1; + } else if ($x < $y) { + return -1; + } else { + return 0; + } + } + + /** + * "Initializes" a new number. This may simply return the + * specified number or it may call a library function for this + * purpose. The base may be ignored depending on the + * implementation. + */ + function init($number, $base = 10) + { + return $number; + } + + /** + * Returns the result of $base mod $modulus. + */ + function mod($base, $modulus) + { + return $base % $modulus; + } + + /** + * Returns the product of $x and $y. + */ + function mul($x, $y) + { + return $x * $y; + } + + /** + * Returns the difference of $x and $y. + */ + function sub($x, $y) + { + return $x - $y; + } + + /** + * Returns $x / $y. + */ + function div($x, $y) + { + return $x / $y; + } + + /** + * Returns ($base to the $exponent power) mod $modulus. In some + * long number implementations, this may be optimized. This + * placeholder implementation performs it manually. + */ + 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; + } +} + +/** + * Exposes BCmath math library functionality. + * + * Auth_OpenID_BcMathWrapper implements the Auth_OpenID_MathWrapper + * interface and wraps the functionality provided by the BCMath + * library. + * + * @package OpenID + */ +class Auth_OpenID_BcMathWrapper extends Auth_OpenID_MathWrapper { + var $type = 'bcmath'; + + function random($min, $max) + { + return mt_rand($min, $max); + } + + 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); + } + + function powmod($base, $exponent, $modulus) + { + if (false && function_exists('bcpowmod')) { + return bcpowmod($base, $exponent, $modulus); + } else { + return parent::powmod($base, $exponent, $modulus); + } + } + +} + +/** + * Exposes GMP math library functionality. + * + * Auth_OpenID_GmpMathWrapper implements the Auth_OpenID_MathWrapper + * interface and wraps the functionality provided by the GMP library. + * + * @package OpenID + */ +class Auth_OpenID_GmpMathWrapper extends Auth_OpenID_MathWrapper { + var $type = 'gmp'; + + function random($min, $max) + { + return gmp_random($max); + } + + 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); + } +} + +$_Auth_OpenID___mathLibrary = null; + +/** + * 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 + * 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. + */ +$_Auth_OpenID_supported_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') + ); + + /** + * Auth_OpenID_MathLibrary checks for the presence of long number + * extension modules and returns an instance of Auth_OpenID_MathWrapper + * which exposes the module's functionality. + * + * @static + * @package OpenID + */ +class Auth_OpenID_MathLibrary { + + /** + * A method to access an available long number implementation. + * + * Checks for the existence of an extension module described by + * the local Auth_OpenID_supported_extensions array and returns an + * instance of a wrapper for that extension module. If no + * extension module is found, an instance of + * Auth_OpenID_MathWrapper is returned, which wraps the native PHP + * integer implementation. The proper calling convention for this + * method is $lib =& Auth_OpenID_MathLibrary::getLibWrapper(). + * + * 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 + * Auth_OpenID_MathWrapper or one of its subclasses + */ + function &getLibWrapper() + { + // 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. + global $_Auth_OpenID___mathLibrary; + + if (defined('Auth_OpenID_NO_MATH_SUPPORT')) { + $_Auth_OpenID___mathLibrary = null; + return $_Auth_OpenID___mathLibrary; + } + + global $_Auth_OpenID_supported_extensions; + + // If this method has not been called before, look at + // $Auth_OpenID_supported_extensions and try to find an + // extension that works. + if (!$_Auth_OpenID___mathLibrary) { + $loaded = false; + $tried = array(); + + foreach ($_Auth_OpenID_supported_extensions as $extension) { + $tried[] = $extension['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) { + $classname = $extension['class']; + $_Auth_OpenID___mathLibrary = new $classname(); + break; + } + } + + // If no extensions were found, fall back to + // Auth_OpenID_MathWrapper so at least some platform-size + // math can be performed. + if (!$_Auth_OpenID___mathLibrary) { + $triedstr = implode(", ", $tried); + $msg = 'This PHP installation has no big integer math ' . + 'library. Define Auth_OpenID_NO_MATH_SUPPORT to use ' . + 'this library in dumb mode. Tried: ' . $triedstr; + trigger_error($msg, E_USER_ERROR); + } + } + + return $_Auth_OpenID___mathLibrary; + } +} + +?>
\ No newline at end of file diff --git a/Auth/OpenID/DiffieHellman.php b/Auth/OpenID/DiffieHellman.php new file mode 100644 index 0000000..f34ab3c --- /dev/null +++ b/Auth/OpenID/DiffieHellman.php @@ -0,0 +1,109 @@ +<?php + +/** + * The OpenID library's Diffie-Hellman implementation. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * Require CryptUtil because we need to get a Auth_OpenID_MathWrapper + * object. + */ +require_once('CryptUtil.php'); + +$_Auth_OpenID_DEFAULT_MOD = '155172898181473697471232257763715539915724801'. +'966915404479707795314057629378541917580651227423698188993727816152646631'. +'438561595825688188889951272158842675419950341258706556549803580104870537'. +'681476726513255747040765857479291291572334510643245094715007229621094194'. +'349783925984760375594985848253359305585439638443'; + +$_Auth_OpenID_DEFAULT_GEN = '2'; + +/** + * The Diffie-Hellman key exchange class. This class relies on + * Auth_OpenID_MathLibrary to perform large number operations. + * + * @package OpenID + */ +class Auth_OpenID_DiffieHellman { + + var $mod; + var $gen; + var $private; + var $lib = null; + + function fromBase64($mod, $gen) + { + if ($mod !== null) { + $mod = Auth_OpenID_CryptUtil::base64ToLong($mod); + } + + if ($gen !== null) { + $gen = Auth_OpenID_CryptUtil::base64ToLong($gen); + } + + return new Auth_OpenID_DiffieHellman($mod, $gen); + } + + function Auth_OpenID_DiffieHellman($mod = null, $gen = null, + $private = null) + { + global $_Auth_OpenID_DEFAULT_MOD, + $_Auth_OpenID_DEFAULT_GEN; + + $this->lib =& Auth_OpenID_MathLibrary::getLibWrapper(); + + if (!$this->lib) { + // This should NEVER occur because even if no math + // extensions can be found, we should get an instance of + // Auth_OpenID_MathWrapper, but if there's a bug in + // Auth_OpenID_MathLibrary::getLibWrapper, it might. + trigger_error("Big integer fallback implementation unavailable.", + E_USER_ERROR); + } + + if ($mod === null) { + $this->mod = $this->lib->init($_Auth_OpenID_DEFAULT_MOD); + } else { + $this->mod = $mod; + } + + if ($gen === null) { + $this->gen = $this->lib->init($_Auth_OpenID_DEFAULT_GEN); + } else { + $this->gen = $gen; + } + + $this->private = + ($private === null) ? $this->lib->random(1, $this->mod) : $private; + + $this->public = $this->lib->powmod($this->gen, $this->private, + $this->mod); + } + + function getSharedSecret($composite) + { + return $this->lib->powmod($composite, $this->private, $this->mod); + } + + function getPublicKey() + { + return $this->public; + } + + function xorSecret($composite, $secret) + { + $dh_shared = $this->getSharedSecret($composite); + $sha1_dh_shared = Auth_OpenID_CryptUtil::sha1( + Auth_OpenID_CryptUtil::longToBinary($dh_shared)); + return Auth_OpenID_CryptUtil::strxor($secret, $sha1_dh_shared); + } +} diff --git a/Auth/OpenID/HMACSHA1.php b/Auth/OpenID/HMACSHA1.php new file mode 100644 index 0000000..9873f9a --- /dev/null +++ b/Auth/OpenID/HMACSHA1.php @@ -0,0 +1,58 @@ +<?php + +/** + * This is the HMACSHA1 implementation for the OpenID library. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 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('SHA1_BLOCKSIZE', 64); + +if (!function_exists('sha1')) { + // XXX: include the SHA1 code from Dan Libby's OpenID library + trigger_error('No SHA1 function found', E_USER_ERROR); +} else { + function Auth_OpenID_sha1_raw($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. + * + * @ignore + */ +function Auth_OpenID_HMACSHA1($key, $text) +{ + if (strlen($key) > SHA1_BLOCKSIZE) { + $key = Auth_OpenID_sha1_raw($key, true); + } + + $key = str_pad($key, SHA1_BLOCKSIZE, chr(0x00)); + $ipad = str_repeat(chr(0x36), SHA1_BLOCKSIZE); + $opad = str_repeat(chr(0x5c), SHA1_BLOCKSIZE); + $hash1 = Auth_OpenID_sha1_raw(($key ^ $ipad) . $text, true); + $hmac = Auth_OpenID_sha1_raw(($key ^ $opad) . $hash1, true); + return $hmac; +} + +?>
\ No newline at end of file diff --git a/Auth/OpenID/KVForm.php b/Auth/OpenID/KVForm.php new file mode 100644 index 0000000..1b690e5 --- /dev/null +++ b/Auth/OpenID/KVForm.php @@ -0,0 +1,102 @@ +<?php + +/** + * This is the KVForm module. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * The Auth_OpenID_KVForm class. + * + * @package OpenID + */ +class Auth_OpenID_KVForm { + function arrayToKV($values) + { + if ($values === null) { + return null; + } + + $serialized = ''; + foreach ($values as $key => $value) { + if (is_array($value)) { + list($key, $value) = $value; + } + + if (strpos($key, ':') !== false) { + trigger_error('":" in key:' . addslashes($key), + E_USER_WARNING); + return null; + } + + if (strpos($key, "\n") !== false) { + trigger_error('"\n" in key:' . addslashes($key), + E_USER_WARNING); + return null; + } + + if (strpos($value, "\n") !== false) { + trigger_error('"\n" in value:' . addslashes($value), + E_USER_WARNING); + return null; + } + $serialized .= "$key:$value\n"; + } + return $serialized; + } + + function kvToArray($kvs) + { + $lines = explode("\n", $kvs); + + $last = array_pop($lines); + if ($last !== '') { + trigger_error('No newline at end of kv string:' . addslashes($kvs), + E_USER_WARNING); + array_push($lines, $last); + } + + $values = array(); + + for ($lineno = 0; $lineno < count($lines); $lineno++) { + $line = $lines[$lineno]; + $kv = explode(':', $line, 2); + if (count($kv) != 2) { + $esc = addslashes($line); + trigger_error("No colon on line $lineno: $esc", + E_USER_WARNING); + continue; + } + + $key = $kv[0]; + $tkey = trim($key); + if ($tkey != $key) { + $esc = addslashes($key); + trigger_error("Whitespace in key on line $lineno: '$esc'", + E_USER_WARNING); + } + + $value = $kv[1]; + $tval = trim($value); + if ($tval != $value) { + $esc = addslashes($value); + trigger_error("Whitespace in value on line $lineno: '$esc'", + E_USER_WARNING); + } + + $values[$tkey] = $tval; + } + + return $values; + } +} + +?>
\ No newline at end of file diff --git a/Auth/OpenID/OIDUtil.php b/Auth/OpenID/OIDUtil.php new file mode 100644 index 0000000..5a8c3b6 --- /dev/null +++ b/Auth/OpenID/OIDUtil.php @@ -0,0 +1,283 @@ +<?php + +/** + * OIDUtil: URL manipulation utility functions for the OpenID library. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. <openid@janrain.com> + * @copyright 2005 Janrain, Inc. + * @license http://www.gnu.org/copyleft/lesser.html LGPL + */ + +/** + * Some constants for string checking. + */ +$_Auth_OpenID_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; +$_Auth_OpenID_digits = "0123456789"; +$_Auth_OpenID_punct = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; + +/** + * Convenience function for getting array values. + */ +function Auth_OpenID_array_get($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_array_get expected " . + "array as first parameter", E_USER_WARNING); + return false; + } +} + + +/** + * Prints the specified message using trigger_error(E_USER_NOTICE). + */ +function Auth_OpenID_log($message, $unused_level = 0) +{ + trigger_error($message, E_USER_NOTICE); +} + +/** + * Implements the PHP 5 'http_build_query' functionality. + * + * @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 Auth_OpenID_http_build_query($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 Auth_OpenID_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_http_build_query($args); +} + +/** + * Converts the specified string to a base64 representation. + */ +function Auth_OpenID_toBase64($s) +{ + return base64_encode($s); +} + +/** + * Returns the original string representation of the specified + * base64-encoded string. + */ +function Auth_OpenID_fromBase64($s) +{ + return base64_decode($s); +} + +/** + * 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 + */ +function Auth_OpenID_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. + * + * @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 Auth_OpenID_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. + * + * @param string $url The URL to be normalized. + * @return mixed $new_url The URL after normalization, or null if $url + * was malformed. + */ +function Auth_OpenID_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('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/Auth/OpenID/Store/DumbStore.php b/Auth/OpenID/Store/DumbStore.php new file mode 100644 index 0000000..b7ef9c2 --- /dev/null +++ b/Auth/OpenID/Store/DumbStore.php @@ -0,0 +1,117 @@ +<?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('Interface.php'); +require_once('Auth/OpenID/CryptUtil.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 (though only within the + * lifespan of the tokens), 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 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 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_CryptUtil::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 during the lifespan of the token. + */ + 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/Auth/OpenID/Store/FileStore.php b/Auth/OpenID/Store/FileStore.php new file mode 100644 index 0000000..9505dc9 --- /dev/null +++ b/Auth/OpenID/Store/FileStore.php @@ -0,0 +1,652 @@ +<?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('Interface.php'); +require_once('Auth/OpenID/OIDUtil.php'); + +function Auth_OpenID_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_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; + } +} + +function Auth_OpenID_mkstemp($dir) +{ + foreach (range(0, 4) as $i) { + $name = tempnam($dir, "php_openid_filestore_"); + + if ($name !== false) { + return $name; + } + } + return false; +} + +function Auth_OpenID_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; +} + +function Auth_OpenID_listdir($dir) +{ + $handle = opendir($dir); + $files = array(); + while (false !== ($filename = readdir($handle))) { + $files[] = $filename; + } + return $files; +} + +function _isFilenameSafe($char) +{ + global $_Auth_OpenID_letters, $_Auth_OpenID_digits; + $_Auth_OpenID_filename_allowed = $_Auth_OpenID_letters . + $_Auth_OpenID_digits . "."; + return (strpos($_Auth_OpenID_filename_allowed, $char) !== false); +} + +function _safe64($str) +{ + $h64 = Auth_OpenID_toBase64(Auth_OpenID_CryptUtil::sha1($str)); + $h64 = str_replace('+', '_', $h64); + $h64 = str_replace('/', '.', $h64); + $h64 = str_replace('=', '', $h64); + return $h64; +} + +function _filenameEscape($str) +{ + $filename = ""; + for ($i = 0; $i < strlen($str); $i++) { + $c = $str[$i]; + if (_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. + * + * @return bool $result True if the file was present, false if not. + */ +function _removeIfPresent($filename) +{ + return @unlink($filename); +} + +/** + * 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. + */ +function _ensureDir($dir_name) +{ + if (@mkdir($dir_name) || is_dir($dir_name)) { + return true; + } else { + return false; + } +} + +/** + * 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. + * + * Methods of this object can raise OSError if unexpected filesystem + * conditions, such as bad permissions or missing directories, occur. + * + * @package OpenID + */ +class Auth_OpenID_FileStore extends Auth_OpenID_OpenIDStore { + + /** + * Initializes a new FileOpenIDStore. 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) + { + $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 + + $this->_setup(); + } + + function destroy() + { + Auth_OpenID_rmtree($this->directory); + $this->active = false; + } + + /** + * Make sure that the directories in which we store our data + * exist. + * + * @access private + */ + function _setup() + { + _ensureDir(dirname($this->auth_key_name)); + _ensureDir($this->nonce_dir); + _ensureDir($this->association_dir); + _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_mkstemp($dir = $this->temp_dir); + $file_obj = @fopen($name, 'wb'); + if ($file_obj !== false) { + return array($file_obj, $name); + } else { + _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); + + if (!link($tmp, $this->auth_key_name)) { + // 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(); + + if (!$auth_key) { + return null; + } else { + _removeIfPresent($tmp); + } + } + + 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 = _filenameEscape($parts[0]); + $url_hash = _safe64($server_url); + if ($handle) { + $handle_hash = _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 null; + } + + $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 null; + } + + fwrite($tmp_file, $association_s); + + fflush($tmp_file); + + fclose($tmp_file); + + if (!rename($tmp, $filename)) { + // We only expect EEXIST to happen only on Windows. It's + // possible that we will succeed in unlinking the existing + // file, but not in putting the temporary file in place. + unlink($filename); + + // Now the target should not exist. Try renaming again, + // giving up if it fails. + if (!rename($tmp, $filename)) { + _removeIfPresent($tmp); + return null; + } + } + + // If there was an error, don't leave the temporary file + // around. + _removeIfPresent($tmp); + } + + /** + * Retrieve an association. If no handle is specified, return the + * association with the latest expiration. + * + * @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_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) { + _removeIfPresent($filename); + return null; + } + + if ($association->getExpiresIn() == 0) { + _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 _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_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) { + _removeIfPresent($filename); + } + } + } + + $association_filenames = Auth_OpenID_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) { + _removeIfPresent($association_filename); + } else { + if ($association->getExpiresIn() == 0) { + _removeIfPresent($association_filename); + } + } + } + } + } +} + +?> diff --git a/Auth/OpenID/Store/Interface.php b/Auth/OpenID/Store/Interface.php new file mode 100644 index 0000000..0500e44 --- /dev/null +++ b/Auth/OpenID/Store/Interface.php @@ -0,0 +1,179 @@ +<?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. + * + * @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 $AUTH_KEY_LEN bytes long. + * + * @return string The key. It should be $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 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; + } +} +?>
\ No newline at end of file diff --git a/Auth/OpenID/Store/SQLStore.php b/Auth/OpenID/Store/SQLStore.php new file mode 100644 index 0000000..04945ed --- /dev/null +++ b/Auth/OpenID/Store/SQLStore.php @@ -0,0 +1,16 @@ +<?php + +/** + * A base class for 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 + */ + +?> |