* @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/Discover.php"; require_once "Services/Yadis/Manager.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 * {@link Auth_OpenID_Consumer} instance is in immediate mode, and the * identity server sends back a URL to send the user to to complete his * or her login. */ define('Auth_OpenID_SETUP_NEEDED', 'setup needed'); /** * This is the status code beginAuth returns when the page fetched * from the entered OpenID URL doesn't contain the necessary link tags * to function as an identity page. */ define('Auth_OpenID_PARSE_ERROR', 'parse error'); /** * This is the characters that the nonces are made from. */ define('Auth_OpenID_DEFAULT_NONCE_CHRS',"abcdefghijklmnopqrstuvwxyz" . "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); /** * 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 /** * An OpenID consumer implementation that performs discovery and does * session management. See the Consumer.php file documentation for * more information. * * @package OpenID */ class Auth_OpenID_Consumer { /** * @access private */ var $session_key_prefix = "_openid_consumer_"; /** * @access private */ var $_token_suffix = "last_token"; /** * Initialize a Consumer instance. * * You should create a new instance of the Consumer object with * every HTTP request that handles OpenID transactions. * * @param Auth_OpenID_OpenIDStore $store This must be an object * that implements the interface in {@link * Auth_OpenID_OpenIDStore}. Several concrete implementations are * provided, to cover most common use cases. For stores backed by * MySQL, PostgreSQL, or SQLite, see the {@link * Auth_OpenID_SQLStore} class and its sublcasses. For a * filesystem-backed store, see the {@link Auth_OpenID_FileStore} * module. As a last resort, if it isn't possible for the server * to store state at all, an instance of {@link * Auth_OpenID_DumbStore} can be used. 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 mixed session An object which implements the interface * of the Services_Yadis_Session class. Particularly, this object * is expected to have these methods: get($key), set($key, * $value), and del($key). This defaults to a session object * which wraps PHP's native session machinery. You should only * need to pass something here if you have your own sessioning * implementation. */ function Auth_OpenID_Consumer(&$store, $session = null) { if ($session === null) { $session = new Services_Yadis_PHPSession(); } $this->session =& $session; $this->consumer =& new Auth_OpenID_GenericConsumer($store); $this->_token_key = $this->session_key_prefix . $this->_token_suffix; } /** * Start the OpenID authentication process. See steps 1-2 in the * overview at the top of this file. * * @param User_url: Identity URL given by the user. This method * performs a textual transformation of the URL to try and make * sure it is normalized. For example, a user_url of example.com * will be normalized to http://example.com/ normalizing and * resolving any redirects the server might issue. * * @return Auth_OpenID_AuthRequest $auth_request An object * containing the discovered information will be returned, with a * method for building a redirect URL to the server, as described * in step 3 of the overview. This object may also be used to add * extension arguments to the request, using its 'addExtensionArg' * method. */ function begin($user_url) { global $_yadis_available; $openid_url = Auth_OpenID::normalizeUrl($user_url); if ($_yadis_available) { $disco = new Services_Yadis_Discovery($this->session, $openid_url, $this->session_key_prefix); $endpoint = $disco->getNextService('Auth_OpenID_discoverWithYadis', $this->consumer->fetcher); } else { $endpoint = null; $result = Auth_OpenID_discover($openid_url, $this->consumer->fetcher); if ($result !== null) { list($temp, $endpoints, $http_response) = $result; $endpoint = $endpoints[0]; } } if ($endpoint === null) { return null; } else { return $this->beginWithoutDiscovery($endpoint); } } /** * Start OpenID verification without doing OpenID server * discovery. This method is used internally by Consumer.begin * after discovery is performed, and exists to provide an * interface for library users needing to perform their own * discovery. * * @param Auth_OpenID_ServiceEndpoint $endpoint an OpenID service * endpoint descriptor. * * @return Auth_OpenID_AuthRequest $auth_request An OpenID * authentication request object. */ function &beginWithoutDiscovery($endpoint) { $auth_req = $this->consumer->begin($endpoint); $this->session->set($this->_token_key, $auth_req->token); return $auth_req; } /** * Called to interpret the server's response to an OpenID * request. It is called in step 4 of the flow described in the * consumer overview. * * @param array $query An array of the query parameters (key => * value pairs) for this HTTP request. * * @return Auth_OpenID_ConsumerResponse $response A instance of an * Auth_OpenID_ConsumerResponse subclass. The type of response is * indicated by the status attribute, which will be one of * SUCCESS, CANCEL, FAILURE, or SETUP_NEEDED. */ function complete($query) { global $_yadis_available; $token = $this->session->get($this->_token_key); if ($token === null) { $response = new Auth_OpenID_FailureResponse(null, 'No session state found'); } else { $response = $this->consumer->complete($query, $token); } if (in_array($response->status, array('success', 'cancel'))) { if ($_yadis_available && $response->identity_url !== null) { $disco = new Services_Yadis_Discovery($this->session, $response->identity_url, $this->session_key_prefix); $disco->cleanup(); } } return $response; } } /** * This class is the interface to the OpenID consumer logic. * Instances of it maintain no per-request state, so they can be * reused (or even used by multiple threads concurrently) as needed. * * @package OpenID * @access private */ class Auth_OpenID_GenericConsumer { /** * This consumer's store object. */ var $store; /** * @access private */ var $_use_assocs; /** * This is the number of characters in the generated nonce for * each transaction. */ var $nonce_len = 8; /** * What characters are allowed in nonces */ var $nonce_chrs = Auth_OpenID_DEFAULT_NONCE_CHRS; /** * 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 {@link Auth_OpenID_Consumer} * instance to access the library. * * @param Auth_OpenID_OpenIDStore $store This must be an object * that implements the interface in {@link Auth_OpenID_OpenIDStore}. * Several concrete implementations are provided, to cover most common use * cases. For stores backed by MySQL, PostgreSQL, or SQLite, see * the {@link Auth_OpenID_SQLStore} class and its sublcasses. For a * filesystem-backed store, see the {@link Auth_OpenID_FileStore} module. * As a last resort, if it isn't possible for the server to store * state at all, an instance of {@link Auth_OpenID_DumbStore} can be used. * 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 bool $immediate This is an optional boolean value. It * controls whether the library uses immediate mode, as explained * in the module description. The default value is False, which * disables immediate mode. */ function Auth_OpenID_GenericConsumer(&$store) { 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()); $this->fetcher = Auth_OpenID::getHTTPFetcher(); } function begin($service_endpoint) { $nonce = $this->_createNonce(); $token = $this->_genToken($nonce, $service_endpoint->identity_url, $service_endpoint->getServerID(), $service_endpoint->server_url); $assoc = $this->_getAssociation($service_endpoint->server_url); $r = new Auth_OpenID_AuthRequest($token, $assoc, $service_endpoint); $r->return_to_args['nonce'] = $nonce; return $r; } function complete($query, $token) { $mode = Auth_OpenID::arrayGet($query, 'openid.mode', ''); $pieces = $this->_splitToken($token); if ($pieces === null) { $pieces = array(null, null, null); } list($nonce, $identity_url, $delegate, $server_url) = $pieces; if ($mode == 'cancel') { return new Auth_OpenID_CancelResponse($identity_url); } else if ($mode == 'error') { $error = Auth_OpenID::arrayGet($query, 'openid.error'); return new Auth_OpenID_FailureResponse($identity_url, $error); } else if ($mode == 'id_res') { if ($identity_url === null) { return new Auth_OpenID_FailureResponse($identity_url, "No session state found"); } $response = $this->_doIdRes($query, $identity_url, $delegate, $server_url); if ($response === null) { return new Auth_OpenID_FailureResponse($identity_url, "HTTP request failed"); } if ($response->status == Auth_OpenID_SUCCESS) { return $this->_checkNonce($response, Auth_OpenID::arrayGet($query, 'nonce')); } else { return $response; } } else { return new Auth_OpenID_FailureResponse($identity_url, sprintf("Invalid openid.mode '%s'", $mode)); } } /** * @access private */ function _doIdRes($query, $consumer_id, $server_id, $server_url) { $user_setup_url = Auth_OpenID::arrayGet($query, 'openid.user_setup_url'); if ($user_setup_url !== null) { return new Auth_OpenID_SetupNeededResponse($consumer_id, $user_setup_url); } $return_to = Auth_OpenID::arrayGet($query, 'openid.return_to', null); $server_id2 = Auth_OpenID::arrayGet($query, 'openid.identity', null); $assoc_handle = Auth_OpenID::arrayGet($query, 'openid.assoc_handle', null); if (($return_to === null) || ($server_id === null) || ($assoc_handle === null)) { return new Auth_OpenID_FailureResponse($consumer_id, "Missing required field"); } if ($server_id != $server_id2) { return new Auth_OpenID_FailureResponse($consumer_id, "Server ID (delegate) mismatch"); } $signed = Auth_OpenID::arrayGet($query, 'openid.signed'); $assoc = $this->store->getAssociation($server_url, $assoc_handle); if ($assoc === null) { // It's not an association we know about. Dumb mode is // our only possible path for recovery. if ($this->_checkAuth($query, $server_url)) { return new Auth_OpenID_SuccessResponse($consumer_id, $query, $signed); } else { return new Auth_OpenID_FailureResponse($consumer_id, "Server denied check_authentication"); } } if ($assoc->getExpiresIn() <= 0) { $msg = sprintf("Association with %s expired", $server_url); return new Auth_OpenID_FailureResponse($consumer_id, $msg); } // 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 new Auth_OpenID_FailureResponse($consumer_id, "Missing argument signature"); } $signed_list = explode(",", $signed); $v_sig = $assoc->signDict($signed_list, $query); if ($v_sig != $sig) { return new Auth_OpenID_FailureResponse($consumer_id, "Bad signature"); } return Auth_OpenID_SuccessResponse::fromQuery($consumer_id, $query, $signed); } /** * @access private */ function _checkAuth($query, $server_url) { $request = $this->_createCheckAuthRequest($query); if ($request === null) { return false; } $response = $this->_makeKVPost($request, $server_url); if ($response == null) { return false; } return $this->_processCheckAuthResponse($response, $server_url); } /** * @access private */ function _createCheckAuthRequest($query) { $signed = Auth_OpenID::arrayGet($query, 'openid.signed', null); if ($signed === null) { return null; } $whitelist = array('assoc_handle', 'sig', 'signed', 'invalidate_handle'); $signed = array_merge(explode(",", $signed), $whitelist); $check_args = array(); foreach ($query as $key => $value) { if (in_array(substr($key, 7), $signed)) { $check_args[$key] = $value; } } $check_args['openid.mode'] = 'check_authentication'; return $check_args; } /** * @access private */ function _processCheckAuthResponse($response, $server_url) { $is_valid = Auth_OpenID::arrayGet($response, 'is_valid', 'false'); if ($is_valid == 'true') { $invalidate_handle = Auth_OpenID::arrayGet($response, 'invalidate_handle'); if ($invalidate_handle !== null) { $this->store->removeAssociation($server_url, $invalidate_handle); } return true; } return false; } /** * @access private */ function _makeKVPost($args, $server_url) { $mode = $args['openid.mode']; $pairs = array(); foreach ($args as $k => $v) { $v = urlencode($v); $pairs[] = "$k=$v"; } $body = implode("&", $pairs); $resp = $this->fetcher->post($server_url, $body); if ($resp === null) { return null; } list($code, $url, $resp_body) = $resp; $response = Auth_OpenID_KVForm::toArray($resp_body); if ($code == 400) { return null; } else if ($code != 200) { return null; } return $response; } /** * @access private */ function _checkNonce($response, $nonce) { $parsed_url = parse_url($response->getReturnTo()); $query_str = @$parsed_url['query']; $query = array(); parse_str($query_str, $query); $found = false; foreach ($query as $k => $v) { if ($k == 'nonce') { if ($v != $nonce) { return new Auth_OpenID_FailureResponse( $response->identity_url, "Nonce mismatch"); } else { $found = true; break; } } } if (!$found) { return new Auth_OpenID_FailureResponse($response->identity_url, sprintf("Nonce missing from return_to: %s", $response->getReturnTo())); } if (!$this->store->useNonce($nonce)) { return new Auth_OpenID_FailureResponse($response->identity_url, "Nonce missing from store"); } return $response; } /** * @access private */ function _createNonce() { $nonce = Auth_OpenID_CryptUtil::randomString($this->nonce_len, $this->nonce_chrs); $this->store->storeNonce($nonce); return $nonce; } /** * @access protected */ function _createDiffieHellman() { return new Auth_OpenID_DiffieHellman(); } /** * @access private */ function _getAssociation($server_url, $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::httpBuildQuery($args); $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_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; } } /** * This class represents an authentication request from a consumer to * an OpenID server. * * @package OpenID */ class Auth_OpenID_AuthRequest { /** * Initialize an authentication request with the specified token, * association, and endpoint. * * Users of this library should not create instances of this * class. Instances of this class are created by the library when * needed. */ function Auth_OpenID_AuthRequest($token, $assoc, $endpoint) { $this->assoc = $assoc; $this->endpoint = $endpoint; $this->extra_args = array(); $this->return_to_args = array(); $this->token = $token; } /** * Add an extension argument to this OpenID authentication * request. * * Use caution when adding arguments, because they will be * URL-escaped and appended to the redirect URL, which can easily * get quite long. * * @param string $namespace The namespace for the extension. For * example, the simple registration extension uses the namespace * 'sreg'. * * @param string $key The key within the extension namespace. For * example, the nickname field in the simple registration * extension's key is 'nickname'. * * @param string $value The value to provide to the server for * this argument. */ function addExtensionArg($namespace, $key, $value) { $arg_name = implode('.', array('openid', $namespace, $key)); $this->extra_args[$arg_name] = $value; } /** * Compute the appropriate redirection URL for this request based * on a specified trust root and return-to. * * @param string $trust_root The trust root URI for your * application. * * @param string$ $return_to The return-to URL to be used when the * OpenID server redirects the user back to your site. * * @return string $redirect_url The resulting redirect URL that * you should send to the user agent. */ function redirectURL($trust_root, $return_to, $immediate=false) { if ($immediate) { $mode = 'checkid_immediate'; } else { $mode = 'checkid_setup'; } $return_to = Auth_OpenID::appendArgs($return_to, $this->return_to_args); $redir_args = array( 'openid.mode' => $mode, 'openid.identity' => $this->endpoint->getServerID(), 'openid.return_to' => $return_to, 'openid.trust_root' => $trust_root); if ($this->assoc) { $redir_args['openid.assoc_handle'] = $this->assoc->handle; } $redir_args = array_merge($redir_args, $this->extra_args); return Auth_OpenID::appendArgs($this->endpoint->server_url, $redir_args); } } /** * The base class for responses from the Auth_OpenID_Consumer. * * @package OpenID */ class Auth_OpenID_ConsumerResponse { var $status = null; } /** * A response with a status of Auth_OpenID_SUCCESS. Indicates that * this request is a successful acknowledgement from the OpenID server * that the supplied URL is, indeed controlled by the requesting * agent. This has three relevant attributes: * * identity_url - The identity URL that has been authenticated * * signed_args - The arguments in the server's response that were * signed and verified. * * status - Auth_OpenID_SUCCESS. * * @package OpenID */ class Auth_OpenID_SuccessResponse extends Auth_OpenID_ConsumerResponse { var $status = 'success'; /** * @access private */ function Auth_OpenID_SuccessResponse($identity_url, $signed_args) { $this->identity_url = $identity_url; $this->signed_args = $signed_args; } /** * @access private */ function fromQuery($identity_url, $query, $signed) { $signed_args = array(); foreach (explode(",", $signed) as $field_name) { $field_name = 'openid.' . $field_name; $signed_args[$field_name] = Auth_OpenID::arrayGet($query, $field_name, ''); } return new Auth_OpenID_SuccessResponse($identity_url, $signed_args); } /** * Extract signed extension data from the server's response. * * @param string $prefix The extension namespace from which to * extract the extension data. */ function extensionResponse($prefix) { $response = array(); $prefix = sprintf('openid.%s.', $prefix); $prefix_len = strlen($prefix); foreach ($this->signed_args as $k => $v) { if (strpos($k, $prefix) === 0) { $response_key = substring($k, $prefix_len); $response[$response_key] = $v; } } return $response; } /** * Get the openid.return_to argument from this response. * * This is useful for verifying that this request was initiated by * this consumer. * * @return string $return_to The return_to URL supplied to the * server on the initial request, or null if the response did not * contain an 'openid.return_to' argument. */ function getReturnTo() { return Auth_OpenID::arrayGet($this->signed_args, 'openid.return_to'); } } /** * A response with a status of Auth_OpenID_FAILURE. Indicates that the * OpenID protocol has failed. This could be locally or remotely * triggered. This has three relevant attributes: * * identity_url - The identity URL for which authentication was * attempted, if it can be determined. Otherwise, null. * * message - A message indicating why the request failed, if one is * supplied. Otherwise, null. * * status - Auth_OpenID_FAILURE. * * @package OpenID */ class Auth_OpenID_FailureResponse extends Auth_OpenID_ConsumerResponse { var $status = 'failure'; function Auth_OpenID_FailureResponse($identity_url = null, $message = null) { $this->identity_url = $identity_url; $this->message = $message; } } /** * A response with a status of Auth_OpenID_CANCEL. Indicates that the * user cancelled the OpenID authentication request. This has two * relevant attributes: * * identity_url - The identity URL for which authentication was * attempted, if it can be determined. Otherwise, null. * * status - Auth_OpenID_SUCCESS. * * @package OpenID */ class Auth_OpenID_CancelResponse extends Auth_OpenID_ConsumerResponse { var $status = 'cancelled'; function Auth_OpenID_CancelResponse($identity_url = null) { $this->identity_url = $identity_url; } } /** * A response with a status of Auth_OpenID_SETUP_NEEDED. Indicates * that the request was in immediate mode, and the server is unable to * authenticate the user without further interaction. * * identity_url - The identity URL for which authentication was * attempted. * * setup_url - A URL that can be used to send the user to the server * to set up for authentication. The user should be redirected in to * the setup_url, either in the current window or in a new browser * window. * * status - Auth_OpenID_SETUP_NEEDED. * * @package OpenID */ class Auth_OpenID_SetupNeededResponse extends Auth_OpenID_ConsumerResponse { var $status = 'setup_needed'; function Auth_OpenID_SetupNeededResponse($identity_url = null, $setup_url = null) { $this->identity_url = $identity_url; $this->setup_url = $setup_url; } } ?>