* @copyright 2005 Janrain, Inc. * @license http://www.gnu.org/copyleft/lesser.html LGPL */ /** * Require utility classes and functions for the consumer. */ require_once "Auth/OpenID.php"; require_once "Auth/OpenID/HMACSHA1.php"; require_once "Auth/OpenID/Association.php"; require_once "Auth/OpenID/AuthenticationRequest.php"; require_once "Auth/OpenID/CryptUtil.php"; require_once "Auth/OpenID/DiffieHellman.php"; require_once "Auth/OpenID/KVForm.php"; require_once "Auth/OpenID/Util.php"; /** * This is the status code returned when either the of the beginAuth * or completeAuth methods return successfully. */ define('Auth_OpenID_SUCCESS', 'success'); /** * This is the status code completeAuth returns when the value it * received indicated an invalid login. */ define('Auth_OpenID_FAILURE', 'failure'); /** * This is the status code completeAuth returns when the * Auth_OpenID_Consumer instance is in immediate mode, and the identity * server sends back a URL to send the user to to complete his or her * login. */ define('Auth_OpenID_SETUP_NEEDED', 'setup needed'); /** * This is the status code beginAuth returns when the page fetched * from the entered OpenID URL doesn't contain the necessary link tags * to function as an identity page. */ define('Auth_OpenID_PARSE_ERROR', 'parse error'); /** * This is the characters that the nonces are made from. */ define('Auth_OpenID_DEFAULT_NONCE_CHRS',"abcdefghijklmnopqrstuvwxyz" . "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); /** * 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. */ define('Auth_OpenID_DEFAULT_TOKEN_LIFETIME', 60 * 5); // five minutes /** * This is the number of characters in the generated nonce for each * transaction. */ define('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 consumer's store object. */ var $store; /** * @access private */ var $_use_assocs; /** * This consumer's HTTP fetcher object. */ var $fetcher; /** * The consumer's mode. */ var $mode; /** * What characters are allowed in nonces */ var $nonce_chrs = Auth_OpenID_DEFAULT_NONCE_CHRS; /** * How long should an authentication session stay good? * * In units of sections. Shorter times mean less opportunity for * attackers, longer times mean less chance of a user's session * timing out. */ var $token_lifetime = Auth_OpenID_DEFAULT_TOKEN_LIFETIME; /** * 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) { if ($store === null) { trigger_error("Must supply non-null store to create consumer", E_USER_ERROR); return null; } $this->store =& $store; $this->_use_assocs = !(defined('Auth_OpenID_NO_MATH_SUPPORT') || $this->store->isDumb()); if ($fetcher === null) { $this->fetcher = Auth_OpenID::getHTTPFetcher(); } else { $this->fetcher =& $fetcher; } } /** * 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_AuthenticationRequest. 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) { list($status, $info) = $this->fetcher->findIdentityInfo($user_url); if ($status != Auth_OpenID_SUCCESS) { return array($status, $info); } list($user_url, $server_id, $server_url) = $info; $nonce = $this->_generateNonce(); $token = $this->_genToken($nonce, $user_url, $server_id, $server_url); $req = new Auth_OpenID_AuthenticationRequest ($token, $server_id, $server_url, $nonce); return array(Auth_OpenID_SUCCESS, $req); } /** * 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_AuthenticationRequest $auth_request This * must be a Auth_OpenID_AuthenticationRequest 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, $immediate = false) { $assoc = $this->_getAssociation($auth_request->server_url, $replace = 1); if ($assoc === null && $this->_use_assocs) { $msg = "Could not get association for redirection"; trigger_error($msg, E_USER_WARNING); return null; } $mode = $immediate ? 'checkid_immediate' : 'checkid_setup'; $redir_args = array( 'openid.identity' => $auth_request->server_id, 'openid.return_to' => $return_to, 'openid.trust_root' => $trust_root, 'openid.mode' => $mode, ); if ($assoc !== null) { $redir_args['openid.assoc_handle'] = $assoc->handle; } $this->store->storeNonce($auth_request->nonce); return Auth_OpenID_appendArgs($auth_request->server_url, $redir_args); } /** * 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) { $query = Auth_OpenID::fixArgs($query); $mode = Auth_OpenID_arrayGet($query, 'openid.mode', ''); if ($mode == 'cancel') { return array(Auth_OpenID_SUCCESS, null); } elseif ($mode == 'error') { $error = Auth_OpenID_arrayGet($query, 'openid.error', null); if ($error !== null) { trigger_error("In OpenID completeAuth: $error", E_USER_NOTICE); } else { trigger_error("Error response from server", E_USER_NOTICE); } return array(Auth_OpenID_FAILURE, null); } elseif ($mode == 'id_res') { return $this->_doIdRes($token, $query); } else { trigger_error("No openid.mode in response from server", E_USER_NOTICE); return array(Auth_OpenID_FAILURE, null); } } /** * @access private */ function _doIdRes($token, $query) { $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_arrayGet($query, 'openid.return_to', null); $server_id2 = Auth_OpenID_arrayGet($query, 'openid.identity', null); $assoc_handle = Auth_OpenID_arrayGet($query, 'openid.assoc_handle', null); if (($return_to === null) || ($server_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_arrayGet($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_arrayGet($query, 'openid.sig', null); $signed = Auth_OpenID_arrayGet($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) { $signed = Auth_OpenID_arrayGet($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::toArray($ret[2]); $is_valid = Auth_OpenID_arrayGet($results, 'is_valid', 'false'); if ($is_valid == 'true') { $invalidate_handle = Auth_OpenID_arrayGet($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_arrayGet($results, 'error', null); if ($error !== null) { $msg = sprintf("Error message from server during " . "check_authentication: %s", $error); trigger_error($msg, E_USER_NOTICE); } return Auth_OpenID_FAILURE; } /** * @access protected */ function _createDiffieHellman() { return new Auth_OpenID_DiffieHellman(); } /** * @access private */ function _getAssociation($server_url, $replace = false) { if (!$this->_use_assocs) { return null; } $assoc = $this->store->getAssociation($server_url); if (($assoc === null) || ($replace && ($assoc->getExpiresIn() < $this->token_lifetime))) { $args = array( 'openid.mode' => 'associate', 'openid.assoc_type' => 'HMAC-SHA1', ); $dh = $this->_createDiffieHellman(); $args = array_merge($args, $dh->getAssocArgs()); $body = Auth_OpenID_http_build_query($args); $assoc = $this->_fetchAssociation($dh, $server_url, $body); } return $assoc; } /** * @static * @access private */ function _generateNonce() { return Auth_OpenID_randomString(Auth_OpenID_NONCE_LEN, $this->nonce_chrs); } /** * @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_HMACSHA1($this->store->getAuthKey(), $joined); return base64_encode($sig . $joined); } /** * @access private */ function _splitToken($token) { $token = base64_decode($token); if (strlen($token) < 20) { return null; } $sig = substr($token, 0, 20); $joined = substr($token, 20); $check_sig = Auth_OpenID_HMACSHA1($this->store->getAuthKey(), $joined); if ($check_sig != $sig) { return null; } $split = explode("\x00", $joined); if (count($split) != 5) { return null; } $ts = intval($split[0]); if ($ts == 0) { return null; } if ($ts + $this->token_lifetime < time()) { return null; } return array_slice($split, 1); } /** * @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'; trigger_error(sprintf($fmt, $server_url), E_USER_NOTICE); return null; } list($http_code, $url, $data) = $ret; $results = Auth_OpenID_KVForm::toArray($data); if ($http_code == 400) { $error = Auth_OpenID_arrayGet($results, 'error', ''); $fmt = 'Getting association: error returned from server %s: %s'; trigger_error(sprintf($fmt, $server_url, $error), E_USER_NOTICE); return null; } else if ($http_code != 200) { $fmt = 'Getting association: bad status code from server %s: %s'; $msg = sprintf($fmt, $server_url, $http_code); trigger_error($msg, E_USER_NOTICE); return null; } $results = Auth_OpenID_KVForm::toArray($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)) { $fmt = "associate: missing key in response from %s: %s"; $msg = sprintf($fmt, $server_url, $key); trigger_error($msg, E_USER_NOTICE); return null; } } $assoc_type = $results['assoc_type']; if ($assoc_type != 'HMAC-SHA1') { $fmt = 'Unsupported assoc_type returned from server %s: %s'; $msg = sprintf($fmt, $server_url, $assoc_type); trigger_error($msg, E_USER_NOTICE); return null; } $assoc_handle = $results['assoc_handle']; $expires_in = intval(Auth_OpenID_arrayGet($results, 'expires_in', '0')); $session_type = Auth_OpenID_arrayGet($results, 'session_type', null); if ($session_type === null) { $secret = base64_decode($results['mac_key']); } else { $fmt = 'Unsupported session_type returned from server %s: %s'; if ($session_type != 'DH-SHA1') { $msg = sprintf($fmt, $server_url, $session_type); trigger_error($msg, E_USER_NOTICE); return null; } $secret = $dh->consumerFinish($results); } $assoc = Auth_OpenID_Association::fromExpiresIn($expires_in, $assoc_handle, $secret, $assoc_type); $this->store->storeAssociation($server_url, $assoc); return $assoc; } } ?>