From 14c9366aac697e87499c5748b36fa7bf5e6cd320 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Fri, 22 Aug 2008 09:17:14 -0400 Subject: include external libs in a subdir to make install easier darcs-hash:20080822131714-84dde-6978424ded2ed1041a65142a25560654ac717fcd.gz --- extlib/Auth/OpenID/Server.php | 1754 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1754 insertions(+) create mode 100644 extlib/Auth/OpenID/Server.php (limited to 'extlib/Auth/OpenID/Server.php') diff --git a/extlib/Auth/OpenID/Server.php b/extlib/Auth/OpenID/Server.php new file mode 100644 index 000000000..e746bcc57 --- /dev/null +++ b/extlib/Auth/OpenID/Server.php @@ -0,0 +1,1754 @@ + // when request is a checkid_* request + * $response = $request->answer(true); + * // this will a signed 'openid.sreg.timezone' parameter to the response + * response.addField('sreg', 'timezone', 'America/Los_Angeles') + * + * Stores + * + * The OpenID server needs to maintain state between requests in order + * to function. Its mechanism for doing this is called a store. The + * store interface is defined in Interface.php. Additionally, several + * concrete store implementations are provided, so that most sites + * won't need to implement a custom store. For a store backed by flat + * files on disk, see {@link Auth_OpenID_FileStore}. For stores based + * on MySQL, SQLite, or PostgreSQL, see the {@link + * Auth_OpenID_SQLStore} subclasses. + * + * Upgrading + * + * The keys by which a server looks up associations in its store have + * changed in version 1.2 of this library. If your store has entries + * created from version 1.0 code, you should empty it. + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @package OpenID + * @author JanRain, Inc. + * @copyright 2005-2008 Janrain, Inc. + * @license http://www.apache.org/licenses/LICENSE-2.0 Apache + */ + +/** + * Required imports + */ +require_once "Auth/OpenID.php"; +require_once "Auth/OpenID/Association.php"; +require_once "Auth/OpenID/CryptUtil.php"; +require_once "Auth/OpenID/BigMath.php"; +require_once "Auth/OpenID/DiffieHellman.php"; +require_once "Auth/OpenID/KVForm.php"; +require_once "Auth/OpenID/TrustRoot.php"; +require_once "Auth/OpenID/ServerRequest.php"; +require_once "Auth/OpenID/Message.php"; +require_once "Auth/OpenID/Nonce.php"; + +define('AUTH_OPENID_HTTP_OK', 200); +define('AUTH_OPENID_HTTP_REDIRECT', 302); +define('AUTH_OPENID_HTTP_ERROR', 400); + +/** + * @access private + */ +global $_Auth_OpenID_Request_Modes; +$_Auth_OpenID_Request_Modes = array('checkid_setup', + 'checkid_immediate'); + +/** + * @access private + */ +define('Auth_OpenID_ENCODE_KVFORM', 'kfvorm'); + +/** + * @access private + */ +define('Auth_OpenID_ENCODE_URL', 'URL/redirect'); + +/** + * @access private + */ +define('Auth_OpenID_ENCODE_HTML_FORM', 'HTML form'); + +/** + * @access private + */ +function Auth_OpenID_isError($obj, $cls = 'Auth_OpenID_ServerError') +{ + return is_a($obj, $cls); +} + +/** + * An error class which gets instantiated and returned whenever an + * OpenID protocol error occurs. Be prepared to use this in place of + * an ordinary server response. + * + * @package OpenID + */ +class Auth_OpenID_ServerError { + /** + * @access private + */ + function Auth_OpenID_ServerError($message = null, $text = null, + $reference = null, $contact = null) + { + $this->message = $message; + $this->text = $text; + $this->contact = $contact; + $this->reference = $reference; + } + + function getReturnTo() + { + if ($this->message && + $this->message->hasKey(Auth_OpenID_OPENID_NS, 'return_to')) { + return $this->message->getArg(Auth_OpenID_OPENID_NS, + 'return_to'); + } else { + return null; + } + } + + /** + * Returns the return_to URL for the request which caused this + * error. + */ + function hasReturnTo() + { + return $this->getReturnTo() !== null; + } + + /** + * Encodes this error's response as a URL suitable for + * redirection. If the response has no return_to, another + * Auth_OpenID_ServerError is returned. + */ + function encodeToURL() + { + if (!$this->message) { + return null; + } + + $msg = $this->toMessage(); + return $msg->toURL($this->getReturnTo()); + } + + /** + * Encodes the response to key-value form. This is a + * machine-readable format used to respond to messages which came + * directly from the consumer and not through the user-agent. See + * the OpenID specification. + */ + function encodeToKVForm() + { + return Auth_OpenID_KVForm::fromArray( + array('mode' => 'error', + 'error' => $this->toString())); + } + + function toFormMarkup($form_tag_attrs=null) + { + $msg = $this->toMessage(); + return $msg->toFormMarkup($this->getReturnTo(), $form_tag_attrs); + } + + function toHTML($form_tag_attrs=null) + { + return Auth_OpenID::autoSubmitHTML( + $this->toFormMarkup($form_tag_attrs)); + } + + function toMessage() + { + // Generate a Message object for sending to the relying party, + // after encoding. + $namespace = $this->message->getOpenIDNamespace(); + $reply = new Auth_OpenID_Message($namespace); + $reply->setArg(Auth_OpenID_OPENID_NS, 'mode', 'error'); + $reply->setArg(Auth_OpenID_OPENID_NS, 'error', $this->toString()); + + if ($this->contact !== null) { + $reply->setArg(Auth_OpenID_OPENID_NS, 'contact', $this->contact); + } + + if ($this->reference !== null) { + $reply->setArg(Auth_OpenID_OPENID_NS, 'reference', + $this->reference); + } + + return $reply; + } + + /** + * Returns one of Auth_OpenID_ENCODE_URL, + * Auth_OpenID_ENCODE_KVFORM, or null, depending on the type of + * encoding expected for this error's payload. + */ + function whichEncoding() + { + global $_Auth_OpenID_Request_Modes; + + if ($this->hasReturnTo()) { + if ($this->message->isOpenID2() && + (strlen($this->encodeToURL()) > + Auth_OpenID_OPENID1_URL_LIMIT)) { + return Auth_OpenID_ENCODE_HTML_FORM; + } else { + return Auth_OpenID_ENCODE_URL; + } + } + + if (!$this->message) { + return null; + } + + $mode = $this->message->getArg(Auth_OpenID_OPENID_NS, + 'mode'); + + if ($mode) { + if (!in_array($mode, $_Auth_OpenID_Request_Modes)) { + return Auth_OpenID_ENCODE_KVFORM; + } + } + return null; + } + + /** + * Returns this error message. + */ + function toString() + { + if ($this->text) { + return $this->text; + } else { + return get_class($this) . " error"; + } + } +} + +/** + * Error returned by the server code when a return_to is absent from a + * request. + * + * @package OpenID + */ +class Auth_OpenID_NoReturnToError extends Auth_OpenID_ServerError { + function Auth_OpenID_NoReturnToError($message = null, + $text = "No return_to URL available") + { + parent::Auth_OpenID_ServerError($message, $text); + } + + function toString() + { + return "No return_to available"; + } +} + +/** + * An error indicating that the return_to URL is malformed. + * + * @package OpenID + */ +class Auth_OpenID_MalformedReturnURL extends Auth_OpenID_ServerError { + function Auth_OpenID_MalformedReturnURL($message, $return_to) + { + $this->return_to = $return_to; + parent::Auth_OpenID_ServerError($message, "malformed return_to URL"); + } +} + +/** + * This error is returned when the trust_root value is malformed. + * + * @package OpenID + */ +class Auth_OpenID_MalformedTrustRoot extends Auth_OpenID_ServerError { + function Auth_OpenID_MalformedTrustRoot($message = null, + $text = "Malformed trust root") + { + parent::Auth_OpenID_ServerError($message, $text); + } + + function toString() + { + return "Malformed trust root"; + } +} + +/** + * The base class for all server request classes. + * + * @package OpenID + */ +class Auth_OpenID_Request { + var $mode = null; +} + +/** + * A request to verify the validity of a previous response. + * + * @package OpenID + */ +class Auth_OpenID_CheckAuthRequest extends Auth_OpenID_Request { + var $mode = "check_authentication"; + var $invalidate_handle = null; + + function Auth_OpenID_CheckAuthRequest($assoc_handle, $signed, + $invalidate_handle = null) + { + $this->assoc_handle = $assoc_handle; + $this->signed = $signed; + if ($invalidate_handle !== null) { + $this->invalidate_handle = $invalidate_handle; + } + $this->namespace = Auth_OpenID_OPENID2_NS; + $this->message = null; + } + + function fromMessage($message, $server=null) + { + $required_keys = array('assoc_handle', 'sig', 'signed'); + + foreach ($required_keys as $k) { + if (!$message->getArg(Auth_OpenID_OPENID_NS, $k)) { + return new Auth_OpenID_ServerError($message, + sprintf("%s request missing required parameter %s from \ + query", "check_authentication", $k)); + } + } + + $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS, 'assoc_handle'); + $sig = $message->getArg(Auth_OpenID_OPENID_NS, 'sig'); + + $signed_list = $message->getArg(Auth_OpenID_OPENID_NS, 'signed'); + $signed_list = explode(",", $signed_list); + + $signed = $message; + if ($signed->hasKey(Auth_OpenID_OPENID_NS, 'mode')) { + $signed->setArg(Auth_OpenID_OPENID_NS, 'mode', 'id_res'); + } + + $result = new Auth_OpenID_CheckAuthRequest($assoc_handle, $signed); + $result->message = $message; + $result->sig = $sig; + $result->invalidate_handle = $message->getArg(Auth_OpenID_OPENID_NS, + 'invalidate_handle'); + return $result; + } + + function answer(&$signatory) + { + $is_valid = $signatory->verify($this->assoc_handle, $this->signed); + + // Now invalidate that assoc_handle so it this checkAuth + // message cannot be replayed. + $signatory->invalidate($this->assoc_handle, true); + $response = new Auth_OpenID_ServerResponse($this); + + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'is_valid', + ($is_valid ? "true" : "false")); + + if ($this->invalidate_handle) { + $assoc = $signatory->getAssociation($this->invalidate_handle, + false); + if (!$assoc) { + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'invalidate_handle', + $this->invalidate_handle); + } + } + return $response; + } +} + +/** + * A class implementing plaintext server sessions. + * + * @package OpenID + */ +class Auth_OpenID_PlainTextServerSession { + /** + * An object that knows how to handle association requests with no + * session type. + */ + var $session_type = 'no-encryption'; + var $needs_math = false; + var $allowed_assoc_types = array('HMAC-SHA1', 'HMAC-SHA256'); + + function fromMessage($unused_request) + { + return new Auth_OpenID_PlainTextServerSession(); + } + + function answer($secret) + { + return array('mac_key' => base64_encode($secret)); + } +} + +/** + * A class implementing DH-SHA1 server sessions. + * + * @package OpenID + */ +class Auth_OpenID_DiffieHellmanSHA1ServerSession { + /** + * An object that knows how to handle association requests with + * the Diffie-Hellman session type. + */ + + var $session_type = 'DH-SHA1'; + var $needs_math = true; + var $allowed_assoc_types = array('HMAC-SHA1'); + var $hash_func = 'Auth_OpenID_SHA1'; + + function Auth_OpenID_DiffieHellmanSHA1ServerSession($dh, $consumer_pubkey) + { + $this->dh = $dh; + $this->consumer_pubkey = $consumer_pubkey; + } + + function getDH($message) + { + $dh_modulus = $message->getArg(Auth_OpenID_OPENID_NS, 'dh_modulus'); + $dh_gen = $message->getArg(Auth_OpenID_OPENID_NS, 'dh_gen'); + + if ((($dh_modulus === null) && ($dh_gen !== null)) || + (($dh_gen === null) && ($dh_modulus !== null))) { + + if ($dh_modulus === null) { + $missing = 'modulus'; + } else { + $missing = 'generator'; + } + + return new Auth_OpenID_ServerError($message, + 'If non-default modulus or generator is '. + 'supplied, both must be supplied. Missing '. + $missing); + } + + $lib =& Auth_OpenID_getMathLib(); + + if ($dh_modulus || $dh_gen) { + $dh_modulus = $lib->base64ToLong($dh_modulus); + $dh_gen = $lib->base64ToLong($dh_gen); + if ($lib->cmp($dh_modulus, 0) == 0 || + $lib->cmp($dh_gen, 0) == 0) { + return new Auth_OpenID_ServerError( + $message, "Failed to parse dh_mod or dh_gen"); + } + $dh = new Auth_OpenID_DiffieHellman($dh_modulus, $dh_gen); + } else { + $dh = new Auth_OpenID_DiffieHellman(); + } + + $consumer_pubkey = $message->getArg(Auth_OpenID_OPENID_NS, + 'dh_consumer_public'); + if ($consumer_pubkey === null) { + return new Auth_OpenID_ServerError($message, + 'Public key for DH-SHA1 session '. + 'not found in query'); + } + + $consumer_pubkey = + $lib->base64ToLong($consumer_pubkey); + + if ($consumer_pubkey === false) { + return new Auth_OpenID_ServerError($message, + "dh_consumer_public is not base64"); + } + + return array($dh, $consumer_pubkey); + } + + function fromMessage($message) + { + $result = Auth_OpenID_DiffieHellmanSHA1ServerSession::getDH($message); + + if (is_a($result, 'Auth_OpenID_ServerError')) { + return $result; + } else { + list($dh, $consumer_pubkey) = $result; + return new Auth_OpenID_DiffieHellmanSHA1ServerSession($dh, + $consumer_pubkey); + } + } + + function answer($secret) + { + $lib =& Auth_OpenID_getMathLib(); + $mac_key = $this->dh->xorSecret($this->consumer_pubkey, $secret, + $this->hash_func); + return array( + 'dh_server_public' => + $lib->longToBase64($this->dh->public), + 'enc_mac_key' => base64_encode($mac_key)); + } +} + +/** + * A class implementing DH-SHA256 server sessions. + * + * @package OpenID + */ +class Auth_OpenID_DiffieHellmanSHA256ServerSession + extends Auth_OpenID_DiffieHellmanSHA1ServerSession { + + var $session_type = 'DH-SHA256'; + var $hash_func = 'Auth_OpenID_SHA256'; + var $allowed_assoc_types = array('HMAC-SHA256'); + + function fromMessage($message) + { + $result = Auth_OpenID_DiffieHellmanSHA1ServerSession::getDH($message); + + if (is_a($result, 'Auth_OpenID_ServerError')) { + return $result; + } else { + list($dh, $consumer_pubkey) = $result; + return new Auth_OpenID_DiffieHellmanSHA256ServerSession($dh, + $consumer_pubkey); + } + } +} + +/** + * A request to associate with the server. + * + * @package OpenID + */ +class Auth_OpenID_AssociateRequest extends Auth_OpenID_Request { + var $mode = "associate"; + + function getSessionClasses() + { + return array( + 'no-encryption' => 'Auth_OpenID_PlainTextServerSession', + 'DH-SHA1' => 'Auth_OpenID_DiffieHellmanSHA1ServerSession', + 'DH-SHA256' => 'Auth_OpenID_DiffieHellmanSHA256ServerSession'); + } + + function Auth_OpenID_AssociateRequest(&$session, $assoc_type) + { + $this->session =& $session; + $this->namespace = Auth_OpenID_OPENID2_NS; + $this->assoc_type = $assoc_type; + } + + function fromMessage($message, $server=null) + { + if ($message->isOpenID1()) { + $session_type = $message->getArg(Auth_OpenID_OPENID_NS, + 'session_type'); + + if ($session_type == 'no-encryption') { + // oidutil.log('Received OpenID 1 request with a no-encryption ' + // 'assocaition session type. Continuing anyway.') + } else if (!$session_type) { + $session_type = 'no-encryption'; + } + } else { + $session_type = $message->getArg(Auth_OpenID_OPENID_NS, + 'session_type'); + if ($session_type === null) { + return new Auth_OpenID_ServerError($message, + "session_type missing from request"); + } + } + + $session_class = Auth_OpenID::arrayGet( + Auth_OpenID_AssociateRequest::getSessionClasses(), + $session_type); + + if ($session_class === null) { + return new Auth_OpenID_ServerError($message, + "Unknown session type " . + $session_type); + } + + $session = call_user_func(array($session_class, 'fromMessage'), + $message); + if (is_a($session, 'Auth_OpenID_ServerError')) { + return $session; + } + + $assoc_type = $message->getArg(Auth_OpenID_OPENID_NS, + 'assoc_type', 'HMAC-SHA1'); + + if (!in_array($assoc_type, $session->allowed_assoc_types)) { + $fmt = "Session type %s does not support association type %s"; + return new Auth_OpenID_ServerError($message, + sprintf($fmt, $session_type, $assoc_type)); + } + + $obj = new Auth_OpenID_AssociateRequest($session, $assoc_type); + $obj->message = $message; + $obj->namespace = $message->getOpenIDNamespace(); + return $obj; + } + + function answer($assoc) + { + $response = new Auth_OpenID_ServerResponse($this); + $response->fields->updateArgs(Auth_OpenID_OPENID_NS, + array( + 'expires_in' => sprintf('%d', $assoc->getExpiresIn()), + 'assoc_type' => $this->assoc_type, + 'assoc_handle' => $assoc->handle)); + + $response->fields->updateArgs(Auth_OpenID_OPENID_NS, + $this->session->answer($assoc->secret)); + + if (! ($this->session->session_type == 'no-encryption' + && $this->message->isOpenID1())) { + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'session_type', + $this->session->session_type); + } + + return $response; + } + + function answerUnsupported($text_message, + $preferred_association_type=null, + $preferred_session_type=null) + { + if ($this->message->isOpenID1()) { + return new Auth_OpenID_ServerError($this->message); + } + + $response = new Auth_OpenID_ServerResponse($this); + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'error_code', 'unsupported-type'); + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'error', $text_message); + + if ($preferred_association_type) { + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'assoc_type', + $preferred_association_type); + } + + if ($preferred_session_type) { + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'session_type', + $preferred_session_type); + } + + return $response; + } +} + +/** + * A request to confirm the identity of a user. + * + * @package OpenID + */ +class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { + /** + * Return-to verification callback. Default is + * Auth_OpenID_verifyReturnTo from TrustRoot.php. + */ + var $verifyReturnTo = 'Auth_OpenID_verifyReturnTo'; + + /** + * The mode of this request. + */ + var $mode = "checkid_setup"; // or "checkid_immediate" + + /** + * Whether this request is for immediate mode. + */ + var $immediate = false; + + /** + * The trust_root value for this request. + */ + var $trust_root = null; + + /** + * The OpenID namespace for this request. + * deprecated since version 2.0.2 + */ + var $namespace; + + function make(&$message, $identity, $return_to, $trust_root = null, + $immediate = false, $assoc_handle = null, $server = null) + { + if ($server === null) { + return new Auth_OpenID_ServerError($message, + "server must not be null"); + } + + if ($return_to && + !Auth_OpenID_TrustRoot::_parse($return_to)) { + return new Auth_OpenID_MalformedReturnURL($message, $return_to); + } + + $r = new Auth_OpenID_CheckIDRequest($identity, $return_to, + $trust_root, $immediate, + $assoc_handle, $server); + + $r->namespace = $message->getOpenIDNamespace(); + $r->message =& $message; + + if (!$r->trustRootValid()) { + return new Auth_OpenID_UntrustedReturnURL($message, + $return_to, + $trust_root); + } else { + return $r; + } + } + + function Auth_OpenID_CheckIDRequest($identity, $return_to, + $trust_root = null, $immediate = false, + $assoc_handle = null, $server = null) + { + $this->namespace = Auth_OpenID_OPENID2_NS; + $this->assoc_handle = $assoc_handle; + $this->identity = $identity; + $this->claimed_id = $identity; + $this->return_to = $return_to; + $this->trust_root = $trust_root; + $this->server =& $server; + + if ($immediate) { + $this->immediate = true; + $this->mode = "checkid_immediate"; + } else { + $this->immediate = false; + $this->mode = "checkid_setup"; + } + } + + function equals($other) + { + return ( + (is_a($other, 'Auth_OpenID_CheckIDRequest')) && + ($this->namespace == $other->namespace) && + ($this->assoc_handle == $other->assoc_handle) && + ($this->identity == $other->identity) && + ($this->claimed_id == $other->claimed_id) && + ($this->return_to == $other->return_to) && + ($this->trust_root == $other->trust_root)); + } + + /* + * Does the relying party publish the return_to URL for this + * response under the realm? It is up to the provider to set a + * policy for what kinds of realms should be allowed. This + * return_to URL verification reduces vulnerability to data-theft + * attacks based on open proxies, corss-site-scripting, or open + * redirectors. + * + * This check should only be performed after making sure that the + * return_to URL matches the realm. + * + * @return true if the realm publishes a document with the + * return_to URL listed, false if not or if discovery fails + */ + function returnToVerified() + { + return call_user_func_array($this->verifyReturnTo, + array($this->trust_root, $this->return_to)); + } + + function fromMessage(&$message, $server) + { + $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode'); + $immediate = null; + + if ($mode == "checkid_immediate") { + $immediate = true; + $mode = "checkid_immediate"; + } else { + $immediate = false; + $mode = "checkid_setup"; + } + + $return_to = $message->getArg(Auth_OpenID_OPENID_NS, + 'return_to'); + + if (($message->isOpenID1()) && + (!$return_to)) { + $fmt = "Missing required field 'return_to' from checkid request"; + return new Auth_OpenID_ServerError($message, $fmt); + } + + $identity = $message->getArg(Auth_OpenID_OPENID_NS, + 'identity'); + $claimed_id = $message->getArg(Auth_OpenID_OPENID_NS, 'claimed_id'); + if ($message->isOpenID1()) { + if ($identity === null) { + $s = "OpenID 1 message did not contain openid.identity"; + return new Auth_OpenID_ServerError($message, $s); + } + } else { + if ($identity && !$claimed_id) { + $s = "OpenID 2.0 message contained openid.identity but not " . + "claimed_id"; + return new Auth_OpenID_ServerError($message, $s); + } else if ($claimed_id && !$identity) { + $s = "OpenID 2.0 message contained openid.claimed_id " . + "but not identity"; + return new Auth_OpenID_ServerError($message, $s); + } + } + + // There's a case for making self.trust_root be a TrustRoot + // here. But if TrustRoot isn't currently part of the + // "public" API, I'm not sure it's worth doing. + if ($message->isOpenID1()) { + $trust_root_param = 'trust_root'; + } else { + $trust_root_param = 'realm'; + } + $trust_root = $message->getArg(Auth_OpenID_OPENID_NS, + $trust_root_param); + if (! $trust_root) { + $trust_root = $return_to; + } + + if (! $message->isOpenID1() && + ($return_to === null) && + ($trust_root === null)) { + return new Auth_OpenID_ServerError($message, + "openid.realm required when openid.return_to absent"); + } + + $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS, + 'assoc_handle'); + + $obj = Auth_OpenID_CheckIDRequest::make($message, + $identity, + $return_to, + $trust_root, + $immediate, + $assoc_handle, + $server); + + if (is_a($obj, 'Auth_OpenID_ServerError')) { + return $obj; + } + + $obj->claimed_id = $claimed_id; + + return $obj; + } + + function idSelect() + { + // Is the identifier to be selected by the IDP? + // So IDPs don't have to import the constant + return $this->identity == Auth_OpenID_IDENTIFIER_SELECT; + } + + function trustRootValid() + { + if (!$this->trust_root) { + return true; + } + + $tr = Auth_OpenID_TrustRoot::_parse($this->trust_root); + if ($tr === false) { + return new Auth_OpenID_MalformedTrustRoot($this->message, + $this->trust_root); + } + + if ($this->return_to !== null) { + return Auth_OpenID_TrustRoot::match($this->trust_root, + $this->return_to); + } else { + return true; + } + } + + /** + * Respond to this request. Return either an + * {@link Auth_OpenID_ServerResponse} or + * {@link Auth_OpenID_ServerError}. + * + * @param bool $allow Allow this user to claim this identity, and + * allow the consumer to have this information? + * + * @param string $server_url DEPRECATED. Passing $op_endpoint to + * the {@link Auth_OpenID_Server} constructor makes this optional. + * + * When an OpenID 1.x immediate mode request does not succeed, it + * gets back a URL where the request may be carried out in a + * not-so-immediate fashion. Pass my URL in here (the fully + * qualified address of this server's endpoint, i.e. + * http://example.com/server), and I will use it as a base for the + * URL for a new request. + * + * Optional for requests where {@link $immediate} is false or + * $allow is true. + * + * @param string $identity The OP-local identifier to answer with. + * Only for use when the relying party requested identifier + * selection. + * + * @param string $claimed_id The claimed identifier to answer + * with, for use with identifier selection in the case where the + * claimed identifier and the OP-local identifier differ, + * i.e. when the claimed_id uses delegation. + * + * If $identity is provided but this is not, $claimed_id will + * default to the value of $identity. When answering requests + * that did not ask for identifier selection, the response + * $claimed_id will default to that of the request. + * + * This parameter is new in OpenID 2.0. + * + * @return mixed + */ + function answer($allow, $server_url = null, $identity = null, + $claimed_id = null) + { + if (!$this->return_to) { + return new Auth_OpenID_NoReturnToError(); + } + + if (!$server_url) { + if ((!$this->message->isOpenID1()) && + (!$this->server->op_endpoint)) { + return new Auth_OpenID_ServerError(null, + "server should be constructed with op_endpoint to " . + "respond to OpenID 2.0 messages."); + } + + $server_url = $this->server->op_endpoint; + } + + if ($allow) { + $mode = 'id_res'; + } else if ($this->message->isOpenID1()) { + if ($this->immediate) { + $mode = 'id_res'; + } else { + $mode = 'cancel'; + } + } else { + if ($this->immediate) { + $mode = 'setup_needed'; + } else { + $mode = 'cancel'; + } + } + + if (!$this->trustRootValid()) { + return new Auth_OpenID_UntrustedReturnURL(null, + $this->return_to, + $this->trust_root); + } + + $response = new Auth_OpenID_ServerResponse($this); + + if ($claimed_id && + ($this->message->isOpenID1())) { + return new Auth_OpenID_ServerError(null, + "claimed_id is new in OpenID 2.0 and not " . + "available for ".$this->namespace); + } + + if ($identity && !$claimed_id) { + $claimed_id = $identity; + } + + if ($allow) { + + if ($this->identity == Auth_OpenID_IDENTIFIER_SELECT) { + if (!$identity) { + return new Auth_OpenID_ServerError(null, + "This request uses IdP-driven identifier selection. " . + "You must supply an identifier in the response."); + } + + $response_identity = $identity; + $response_claimed_id = $claimed_id; + + } else if ($this->identity) { + if ($identity && + ($this->identity != $identity)) { + $fmt = "Request was for %s, cannot reply with identity %s"; + return new Auth_OpenID_ServerError(null, + sprintf($fmt, $this->identity, $identity)); + } + + $response_identity = $this->identity; + $response_claimed_id = $this->claimed_id; + } else { + if ($identity) { + return new Auth_OpenID_ServerError(null, + "This request specified no identity and " . + "you supplied ".$identity); + } + + $response_identity = null; + } + + if (($this->message->isOpenID1()) && + ($response_identity === null)) { + return new Auth_OpenID_ServerError(null, + "Request was an OpenID 1 request, so response must " . + "include an identifier."); + } + + $response->fields->updateArgs(Auth_OpenID_OPENID_NS, + array('mode' => $mode, + 'return_to' => $this->return_to, + 'response_nonce' => Auth_OpenID_mkNonce())); + + if (!$this->message->isOpenID1()) { + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'op_endpoint', $server_url); + } + + if ($response_identity !== null) { + $response->fields->setArg( + Auth_OpenID_OPENID_NS, + 'identity', + $response_identity); + if ($this->message->isOpenID2()) { + $response->fields->setArg( + Auth_OpenID_OPENID_NS, + 'claimed_id', + $response_claimed_id); + } + } + + } else { + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'mode', $mode); + + if ($this->immediate) { + if (($this->message->isOpenID1()) && + (!$server_url)) { + return new Auth_OpenID_ServerError(null, + 'setup_url is required for $allow=false \ + in OpenID 1.x immediate mode.'); + } + + $setup_request =& new Auth_OpenID_CheckIDRequest( + $this->identity, + $this->return_to, + $this->trust_root, + false, + $this->assoc_handle, + $this->server); + $setup_request->message = $this->message; + + $setup_url = $setup_request->encodeToURL($server_url); + + if ($setup_url === null) { + return new Auth_OpenID_NoReturnToError(); + } + + $response->fields->setArg(Auth_OpenID_OPENID_NS, + 'user_setup_url', + $setup_url); + } + } + + return $response; + } + + function encodeToURL($server_url) + { + if (!$this->return_to) { + return new Auth_OpenID_NoReturnToError(); + } + + // Imported from the alternate reality where these classes are + // used in both the client and server code, so Requests are + // Encodable too. That's right, code imported from alternate + // realities all for the love of you, id_res/user_setup_url. + + $q = array('mode' => $this->mode, + 'identity' => $this->identity, + 'claimed_id' => $this->claimed_id, + 'return_to' => $this->return_to); + + if ($this->trust_root) { + if ($this->message->isOpenID1()) { + $q['trust_root'] = $this->trust_root; + } else { + $q['realm'] = $this->trust_root; + } + } + + if ($this->assoc_handle) { + $q['assoc_handle'] = $this->assoc_handle; + } + + $response = new Auth_OpenID_Message( + $this->message->getOpenIDNamespace()); + $response->updateArgs(Auth_OpenID_OPENID_NS, $q); + return $response->toURL($server_url); + } + + function getCancelURL() + { + if (!$this->return_to) { + return new Auth_OpenID_NoReturnToError(); + } + + if ($this->immediate) { + return new Auth_OpenID_ServerError(null, + "Cancel is not an appropriate \ + response to immediate mode \ + requests."); + } + + $response = new Auth_OpenID_Message( + $this->message->getOpenIDNamespace()); + $response->setArg(Auth_OpenID_OPENID_NS, 'mode', 'cancel'); + return $response->toURL($this->return_to); + } +} + +/** + * This class encapsulates the response to an OpenID server request. + * + * @package OpenID + */ +class Auth_OpenID_ServerResponse { + + function Auth_OpenID_ServerResponse(&$request) + { + $this->request =& $request; + $this->fields = new Auth_OpenID_Message($this->request->namespace); + } + + function whichEncoding() + { + global $_Auth_OpenID_Request_Modes; + + if (in_array($this->request->mode, $_Auth_OpenID_Request_Modes)) { + if ($this->fields->isOpenID2() && + (strlen($this->encodeToURL()) > + Auth_OpenID_OPENID1_URL_LIMIT)) { + return Auth_OpenID_ENCODE_HTML_FORM; + } else { + return Auth_OpenID_ENCODE_URL; + } + } else { + return Auth_OpenID_ENCODE_KVFORM; + } + } + + /* + * Returns the form markup for this response. + * + * @return str + */ + function toFormMarkup($form_tag_attrs=null) + { + return $this->fields->toFormMarkup($this->request->return_to, + $form_tag_attrs); + } + + /* + * Returns an HTML document containing the form markup for this + * response that autosubmits with javascript. + */ + function toHTML() + { + return Auth_OpenID::autoSubmitHTML($this->toFormMarkup()); + } + + /* + * Returns True if this response's encoding is ENCODE_HTML_FORM. + * Convenience method for server authors. + * + * @return bool + */ + function renderAsForm() + { + return $this->whichEncoding() == Auth_OpenID_ENCODE_HTML_FORM; + } + + + function encodeToURL() + { + return $this->fields->toURL($this->request->return_to); + } + + function addExtension($extension_response) + { + $extension_response->toMessage($this->fields); + } + + function needsSigning() + { + return $this->fields->getArg(Auth_OpenID_OPENID_NS, + 'mode') == 'id_res'; + } + + function encodeToKVForm() + { + return $this->fields->toKVForm(); + } +} + +/** + * A web-capable response object which you can use to generate a + * user-agent response. + * + * @package OpenID + */ +class Auth_OpenID_WebResponse { + var $code = AUTH_OPENID_HTTP_OK; + var $body = ""; + + function Auth_OpenID_WebResponse($code = null, $headers = null, + $body = null) + { + if ($code) { + $this->code = $code; + } + + if ($headers !== null) { + $this->headers = $headers; + } else { + $this->headers = array(); + } + + if ($body !== null) { + $this->body = $body; + } + } +} + +/** + * Responsible for the signature of query data and the verification of + * OpenID signature values. + * + * @package OpenID + */ +class Auth_OpenID_Signatory { + + // = 14 * 24 * 60 * 60; # 14 days, in seconds + var $SECRET_LIFETIME = 1209600; + + // keys have a bogus server URL in them because the filestore + // really does expect that key to be a URL. This seems a little + // silly for the server store, since I expect there to be only one + // server URL. + var $normal_key = 'http://localhost/|normal'; + var $dumb_key = 'http://localhost/|dumb'; + + /** + * Create a new signatory using a given store. + */ + function Auth_OpenID_Signatory(&$store) + { + // assert store is not None + $this->store =& $store; + } + + /** + * Verify, using a given association handle, a signature with + * signed key-value pairs from an HTTP request. + */ + function verify($assoc_handle, $message) + { + $assoc = $this->getAssociation($assoc_handle, true); + if (!$assoc) { + // oidutil.log("failed to get assoc with handle %r to verify sig %r" + // % (assoc_handle, sig)) + return false; + } + + return $assoc->checkMessageSignature($message); + } + + /** + * Given a response, sign the fields in the response's 'signed' + * list, and insert the signature into the response. + */ + function sign($response) + { + $signed_response = $response; + $assoc_handle = $response->request->assoc_handle; + + if ($assoc_handle) { + // normal mode + $assoc = $this->getAssociation($assoc_handle, false, false); + if (!$assoc || ($assoc->getExpiresIn() <= 0)) { + // fall back to dumb mode + $signed_response->fields->setArg(Auth_OpenID_OPENID_NS, + 'invalidate_handle', $assoc_handle); + $assoc_type = ($assoc ? $assoc->assoc_type : 'HMAC-SHA1'); + + if ($assoc && ($assoc->getExpiresIn() <= 0)) { + $this->invalidate($assoc_handle, false); + } + + $assoc = $this->createAssociation(true, $assoc_type); + } + } else { + // dumb mode. + $assoc = $this->createAssociation(true); + } + + $signed_response->fields = $assoc->signMessage( + $signed_response->fields); + return $signed_response; + } + + /** + * Make a new association. + */ + function createAssociation($dumb = true, $assoc_type = 'HMAC-SHA1') + { + $secret = Auth_OpenID_CryptUtil::getBytes( + Auth_OpenID_getSecretSize($assoc_type)); + + $uniq = base64_encode(Auth_OpenID_CryptUtil::getBytes(4)); + $handle = sprintf('{%s}{%x}{%s}', $assoc_type, intval(time()), $uniq); + + $assoc = Auth_OpenID_Association::fromExpiresIn( + $this->SECRET_LIFETIME, $handle, $secret, $assoc_type); + + if ($dumb) { + $key = $this->dumb_key; + } else { + $key = $this->normal_key; + } + + $this->store->storeAssociation($key, $assoc); + return $assoc; + } + + /** + * Given an association handle, get the association from the + * store, or return a ServerError or null if something goes wrong. + */ + function getAssociation($assoc_handle, $dumb, $check_expiration=true) + { + if ($assoc_handle === null) { + return new Auth_OpenID_ServerError(null, + "assoc_handle must not be null"); + } + + if ($dumb) { + $key = $this->dumb_key; + } else { + $key = $this->normal_key; + } + + $assoc = $this->store->getAssociation($key, $assoc_handle); + + if (($assoc !== null) && ($assoc->getExpiresIn() <= 0)) { + if ($check_expiration) { + $this->store->removeAssociation($key, $assoc_handle); + $assoc = null; + } + } + + return $assoc; + } + + /** + * Invalidate a given association handle. + */ + function invalidate($assoc_handle, $dumb) + { + if ($dumb) { + $key = $this->dumb_key; + } else { + $key = $this->normal_key; + } + $this->store->removeAssociation($key, $assoc_handle); + } +} + +/** + * Encode an {@link Auth_OpenID_ServerResponse} to an + * {@link Auth_OpenID_WebResponse}. + * + * @package OpenID + */ +class Auth_OpenID_Encoder { + + var $responseFactory = 'Auth_OpenID_WebResponse'; + + /** + * Encode an {@link Auth_OpenID_ServerResponse} and return an + * {@link Auth_OpenID_WebResponse}. + */ + function encode(&$response) + { + $cls = $this->responseFactory; + + $encode_as = $response->whichEncoding(); + if ($encode_as == Auth_OpenID_ENCODE_KVFORM) { + $wr = new $cls(null, null, $response->encodeToKVForm()); + if (is_a($response, 'Auth_OpenID_ServerError')) { + $wr->code = AUTH_OPENID_HTTP_ERROR; + } + } else if ($encode_as == Auth_OpenID_ENCODE_URL) { + $location = $response->encodeToURL(); + $wr = new $cls(AUTH_OPENID_HTTP_REDIRECT, + array('location' => $location)); + } else if ($encode_as == Auth_OpenID_ENCODE_HTML_FORM) { + $wr = new $cls(AUTH_OPENID_HTTP_OK, array(), + $response->toFormMarkup()); + } else { + return new Auth_OpenID_EncodingError($response); + } + return $wr; + } +} + +/** + * An encoder which also takes care of signing fields when required. + * + * @package OpenID + */ +class Auth_OpenID_SigningEncoder extends Auth_OpenID_Encoder { + + function Auth_OpenID_SigningEncoder(&$signatory) + { + $this->signatory =& $signatory; + } + + /** + * Sign an {@link Auth_OpenID_ServerResponse} and return an + * {@link Auth_OpenID_WebResponse}. + */ + function encode(&$response) + { + // the isinstance is a bit of a kludge... it means there isn't + // really an adapter to make the interfaces quite match. + if (!is_a($response, 'Auth_OpenID_ServerError') && + $response->needsSigning()) { + + if (!$this->signatory) { + return new Auth_OpenID_ServerError(null, + "Must have a store to sign request"); + } + + if ($response->fields->hasKey(Auth_OpenID_OPENID_NS, 'sig')) { + return new Auth_OpenID_AlreadySigned($response); + } + $response = $this->signatory->sign($response); + } + + return parent::encode($response); + } +} + +/** + * Decode an incoming query into an Auth_OpenID_Request. + * + * @package OpenID + */ +class Auth_OpenID_Decoder { + + function Auth_OpenID_Decoder(&$server) + { + $this->server =& $server; + + $this->handlers = array( + 'checkid_setup' => 'Auth_OpenID_CheckIDRequest', + 'checkid_immediate' => 'Auth_OpenID_CheckIDRequest', + 'check_authentication' => 'Auth_OpenID_CheckAuthRequest', + 'associate' => 'Auth_OpenID_AssociateRequest' + ); + } + + /** + * Given an HTTP query in an array (key-value pairs), decode it + * into an Auth_OpenID_Request object. + */ + function decode($query) + { + if (!$query) { + return null; + } + + $message = Auth_OpenID_Message::fromPostArgs($query); + + if ($message === null) { + /* + * It's useful to have a Message attached to a + * ProtocolError, so we override the bad ns value to build + * a Message out of it. Kinda kludgy, since it's made of + * lies, but the parts that aren't lies are more useful + * than a 'None'. + */ + $old_ns = $query['openid.ns']; + + $query['openid.ns'] = Auth_OpenID_OPENID2_NS; + $message = Auth_OpenID_Message::fromPostArgs($query); + return new Auth_OpenID_ServerError( + $message, + sprintf("Invalid OpenID namespace URI: %s", $old_ns)); + } + + $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode'); + if (!$mode) { + return new Auth_OpenID_ServerError($message, + "No mode value in message"); + } + + if (Auth_OpenID::isFailure($mode)) { + return new Auth_OpenID_ServerError($message, + $mode->message); + } + + $handlerCls = Auth_OpenID::arrayGet($this->handlers, $mode, + $this->defaultDecoder($message)); + + if (!is_a($handlerCls, 'Auth_OpenID_ServerError')) { + return call_user_func_array(array($handlerCls, 'fromMessage'), + array($message, $this->server)); + } else { + return $handlerCls; + } + } + + function defaultDecoder($message) + { + $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode'); + + if (Auth_OpenID::isFailure($mode)) { + return new Auth_OpenID_ServerError($message, + $mode->message); + } + + return new Auth_OpenID_ServerError($message, + sprintf("Unrecognized OpenID mode %s", $mode)); + } +} + +/** + * An error that indicates an encoding problem occurred. + * + * @package OpenID + */ +class Auth_OpenID_EncodingError { + function Auth_OpenID_EncodingError(&$response) + { + $this->response =& $response; + } +} + +/** + * An error that indicates that a response was already signed. + * + * @package OpenID + */ +class Auth_OpenID_AlreadySigned extends Auth_OpenID_EncodingError { + // This response is already signed. +} + +/** + * An error that indicates that the given return_to is not under the + * given trust_root. + * + * @package OpenID + */ +class Auth_OpenID_UntrustedReturnURL extends Auth_OpenID_ServerError { + function Auth_OpenID_UntrustedReturnURL($message, $return_to, + $trust_root) + { + parent::Auth_OpenID_ServerError($message, "Untrusted return_to URL"); + $this->return_to = $return_to; + $this->trust_root = $trust_root; + } + + function toString() + { + return sprintf("return_to %s not under trust_root %s", + $this->return_to, $this->trust_root); + } +} + +/** + * I handle requests for an OpenID server. + * + * Some types of requests (those which are not checkid requests) may + * be handed to my {@link handleRequest} method, and I will take care + * of it and return a response. + * + * For your convenience, I also provide an interface to {@link + * Auth_OpenID_Decoder::decode()} and {@link + * Auth_OpenID_SigningEncoder::encode()} through my methods {@link + * decodeRequest} and {@link encodeResponse}. + * + * All my state is encapsulated in an {@link Auth_OpenID_OpenIDStore}. + * + * Example: + * + *
 $oserver = new Auth_OpenID_Server(Auth_OpenID_FileStore($data_path),
+ *                                   "http://example.com/op");
+ * $request = $oserver->decodeRequest();
+ * if (in_array($request->mode, array('checkid_immediate',
+ *                                    'checkid_setup'))) {
+ *     if ($app->isAuthorized($request->identity, $request->trust_root)) {
+ *         $response = $request->answer(true);
+ *     } else if ($request->immediate) {
+ *         $response = $request->answer(false);
+ *     } else {
+ *         $app->showDecidePage($request);
+ *         return;
+ *     }
+ * } else {
+ *     $response = $oserver->handleRequest($request);
+ * }
+ *
+ * $webresponse = $oserver->encode($response);
+ * + * @package OpenID + */ +class Auth_OpenID_Server { + function Auth_OpenID_Server(&$store, $op_endpoint=null) + { + $this->store =& $store; + $this->signatory =& new Auth_OpenID_Signatory($this->store); + $this->encoder =& new Auth_OpenID_SigningEncoder($this->signatory); + $this->decoder =& new Auth_OpenID_Decoder($this); + $this->op_endpoint = $op_endpoint; + $this->negotiator =& Auth_OpenID_getDefaultNegotiator(); + } + + /** + * Handle a request. Given an {@link Auth_OpenID_Request} object, + * call the appropriate {@link Auth_OpenID_Server} method to + * process the request and generate a response. + * + * @param Auth_OpenID_Request $request An {@link Auth_OpenID_Request} + * returned by {@link Auth_OpenID_Server::decodeRequest()}. + * + * @return Auth_OpenID_ServerResponse $response A response object + * capable of generating a user-agent reply. + */ + function handleRequest($request) + { + if (method_exists($this, "openid_" . $request->mode)) { + $handler = array($this, "openid_" . $request->mode); + return call_user_func($handler, $request); + } + return null; + } + + /** + * The callback for 'check_authentication' messages. + */ + function openid_check_authentication(&$request) + { + return $request->answer($this->signatory); + } + + /** + * The callback for 'associate' messages. + */ + function openid_associate(&$request) + { + $assoc_type = $request->assoc_type; + $session_type = $request->session->session_type; + if ($this->negotiator->isAllowed($assoc_type, $session_type)) { + $assoc = $this->signatory->createAssociation(false, + $assoc_type); + return $request->answer($assoc); + } else { + $message = sprintf('Association type %s is not supported with '. + 'session type %s', $assoc_type, $session_type); + list($preferred_assoc_type, $preferred_session_type) = + $this->negotiator->getAllowedType(); + return $request->answerUnsupported($message, + $preferred_assoc_type, + $preferred_session_type); + } + } + + /** + * Encodes as response in the appropriate format suitable for + * sending to the user agent. + */ + function encodeResponse(&$response) + { + return $this->encoder->encode($response); + } + + /** + * Decodes a query args array into the appropriate + * {@link Auth_OpenID_Request} object. + */ + function decodeRequest($query=null) + { + if ($query === null) { + $query = Auth_OpenID::getQuery(); + } + + return $this->decoder->decode($query); + } +} + +?> -- cgit v1.2.3-54-g00ecf