<?php /** * OpenID server protocol and logic. * * Overview * * An OpenID server must perform three tasks: * * 1. Examine the incoming request to determine its nature and validity. * 2. Make a decision about how to respond to this request. * 3. Format the response according to the protocol. * * The first and last of these tasks may performed by the {@link * Auth_OpenID_Server::decodeRequest()} and {@link * Auth_OpenID_Server::encodeResponse} methods. Who gets to do the * intermediate task -- deciding how to respond to the request -- will * depend on what type of request it is. * * If it's a request to authenticate a user (a 'checkid_setup' or * 'checkid_immediate' request), you need to decide if you will assert * that this user may claim the identity in question. Exactly how you * do that is a matter of application policy, but it generally * involves making sure the user has an account with your system and * is logged in, checking to see if that identity is hers to claim, * and verifying with the user that she does consent to releasing that * information to the party making the request. * * Examine the properties of the {@link Auth_OpenID_CheckIDRequest} * object, and if and when you've come to a decision, form a response * by calling {@link Auth_OpenID_CheckIDRequest::answer()}. * * Other types of requests relate to establishing associations between * client and server and verifing the authenticity of previous * communications. {@link Auth_OpenID_Server} contains all the logic * and data necessary to respond to such requests; just pass it to * {@link Auth_OpenID_Server::handleRequest()}. * * OpenID Extensions * * Do you want to provide other information for your users in addition * to authentication? Version 1.2 of the OpenID protocol allows * consumers to add extensions to their requests. For example, with * sites using the Simple Registration * Extension * (http://www.openidenabled.com/openid/simple-registration-extension/), * a user can agree to have their nickname and e-mail address sent to * a site when they sign up. * * Since extensions do not change the way OpenID authentication works, * code to handle extension requests may be completely separate from * the {@link Auth_OpenID_Request} class here. But you'll likely want * data sent back by your extension to be signed. {@link * Auth_OpenID_ServerResponse} provides methods with which you can add * data to it which can be signed with the other data in the OpenID * signature. * * For example: * * <pre> // 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')</pre> * * 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. <openid@janrain.com> * @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, $claimed_id = null) { $this->namespace = Auth_OpenID_OPENID2_NS; $this->assoc_handle = $assoc_handle; $this->identity = $identity; if ($claimed_id === null) { $this->claimed_id = $identity; } else { $this->claimed_id = $claimed_id; } $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, $this->claimed_id); $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: * * <pre> $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);</pre> * * @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); } } ?>