diff options
author | tailor <cygnus@janrain.com> | 2006-04-19 22:08:15 +0000 |
---|---|---|
committer | tailor <cygnus@janrain.com> | 2006-04-19 22:08:15 +0000 |
commit | f00e38a4f9984ba432a73aa1c791ff7ad196f781 (patch) | |
tree | c6f79d4304e1a27b8c1a1b3d5d9a9edfa270e251 | |
parent | 4cb1c79600ea5d6d977c487c78195c06e20652b2 (diff) | |
download | php-openid-f00e38a4f9984ba432a73aa1c791ff7ad196f781.zip php-openid-f00e38a4f9984ba432a73aa1c791ff7ad196f781.tar.gz php-openid-f00e38a4f9984ba432a73aa1c791ff7ad196f781.tar.bz2 |
[project @ Added new server port and unit tests]
-rw-r--r-- | Auth/OpenID/Server.php | 1152 | ||||
-rw-r--r-- | Auth/OpenID/ServerRequest.php | 119 | ||||
-rw-r--r-- | Tests/Auth/OpenID/Server.php | 1327 |
3 files changed, 1940 insertions, 658 deletions
diff --git a/Auth/OpenID/Server.php b/Auth/OpenID/Server.php index a89a4e1..f20edfe 100644 --- a/Auth/OpenID/Server.php +++ b/Auth/OpenID/Server.php @@ -20,432 +20,886 @@ 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"; -/** - * An object that implements the OpenID protocol for a single URL. - * - * Use this object by calling getOpenIDResponse when you get any - * request for the server URL. - * - * @package OpenID - */ -class Auth_OpenID_Server { +define('AUTH_OPENID_HTTP_OK', 200); +define('AUTH_OPENID_HTTP_REDIRECT', 302); +define('AUTH_OPENID_HTTP_ERROR', 400); - /** - * A store implementing the interface in Auth/OpenID/Interface.php - */ - var $store; - - /** - * The URL of the server that this instance represents. - */ - var $server_url; - - /** - * The server URL with a namespace indicating that this - * association is a shared association. - * - * @access private - */ - var $_normal_key; - - /** - * The server URL with a namespace indicating that this - * association is a private (dumb-mode) association. - * - * @access private - */ - var $_dumb_key; - - /** - * How long an association should be valid for (in seconds) - */ - var $association_lifetime = 1209600; // 14 days, in seconds - - /** - * Constructor. - * - * @param string $server_url The URL of the OpenID server - * - * @param mixed $store The association store for this - * instance. See {@link Auth_OpenID_OpenIDStore}. - */ - function Auth_OpenID_Server($server_url, $store) - { - $this->server_url = $server_url; - $this->store =& $store; +global $_Auth_OpenID_Request_Modes, + $_Auth_OpenID_OpenID_Prefix, + $_Auth_OpenID_Encode_Kvform, + $_Auth_OpenID_Encode_Url; + +$_Auth_OpenID_Request_Modes = array('checkid_setup', + 'checkid_immediate'); +$_Auth_OpenID_OpenID_Prefix = "openid."; +$_Auth_OpenID_Encode_Kvform = array('kfvorm'); +$_Auth_OpenID_Encode_Url = array('URL/redirect'); + +function _isError($obj, $cls = 'Auth_OpenID_ServerError') +{ + return is_a($obj, $cls); +} + +class Auth_OpenID_ServerError { + function Auth_OpenID_ServerError($query = null, $message = null) + { + $this->message = $message; + $this->query = $query; + } + + function hasReturnTo() + { + global $_Auth_OpenID_OpenID_Prefix; + return array_key_exists($_Auth_OpenID_OpenID_Prefix . 'return_to', + $this->query); + } - $this->_normal_key = $server_url . '|normal'; - $this->_dumb_key = $server_url . '|dumb'; - } - - /** - * This is the initial entry point for a server URL. - * - * @param mixed $is_authorized: the name of a callback to use for - * determining if a given identity URL should be authorized. It - * will be called with the identity URL and the trust_root for - * this request. - * - * @param string $method The HTTP method of the current - * request. If omitted, $_SERVER['HTTP_METHOD'] will be used. - * - * @param array $args The arguments parsed from the request. If - * omitted, the arguments in the environment will be used. - * - * @return array $array A pair of elements in which the first is a - * status code and the meaning of the second depends on the - * status. See the status codes defined in this file for - * information about each response. - */ - function getOpenIDResponse($is_authorized=false, $method=null, $args=null) - { - if (!isset($method)) { - $method = $_SERVER['REQUEST_METHOD']; - } - - switch ($method) { - - case 'GET': - // Convert anything that starts with openid_ to openid. - if ($args === null) { - $args = Auth_OpenID::fixArgs($_GET); + function encodeToURL() + { + global $_Auth_OpenID_OpenID_Prefix; + $return_to = Auth_OpenID::arrayGet($this->query, + $_Auth_OpenID_OpenID_Prefix . + 'return_to'); + if (!$return_to) { + return new Auth_OpenID_ServerError(null, "no return_to URL"); + } + + return Auth_OpenID::appendArgs($return_to, + array('openid.mode' => 'error', + 'error' => $this->toString())); + } + + function encodeToKVForm() + { + return Auth_OpenID_KVForm::fromArray( + array('mode' => 'error', + 'error' => $this->toString())); + } + + function whichEncoding() + { + global $_Auth_OpenID_Encode_Url, + $_Auth_OpenID_Encode_Kvform, + $_Auth_OpenID_Request_Modes; + + if ($this->hasReturnTo()) { + return $_Auth_OpenID_Encode_Url; + } + + $mode = Auth_OpenID::arrayGet($this->query, 'openid.mode'); + + if ($mode) { + if (!in_array($mode, $_Auth_OpenID_Request_Modes)) { + return $_Auth_OpenID_Encode_Kvform; } - $request = new Auth_OpenID_ServerRequest($this->server_url, $args); - return $request->retry(&$this, $is_authorized); + } + return null; + } + + function toString() + { + if ($this->message) { + return $this->message; + } else { + return get_class($this) . " error"; + } + } +} + +class Auth_OpenID_MalformedReturnURL extends Auth_OpenID_ServerError { + function Auth_OpenID_MalformedReturnURL($query, $return_to) + { + $this->return_to = $return_to; + parent::Auth_OpenID_ServerError($query, "malformed return_to URL"); + } +} + +class Auth_OpenID_MalformedTrustRoot extends Auth_OpenID_ServerError { + function toString() + { + return "Malformed trust root"; + } +} + +class Auth_OpenID_Request { + var $mode = null; +} + +class Auth_OpenID_CheckAuthRequest extends Auth_OpenID_Request { + var $mode = "check_authentication"; + var $invalidate_handle = null; + + function Auth_OpenID_CheckAuthRequest($assoc_handle, $sig, $signed, + $invalidate_handle = null) + { + $this->assoc_handle = $assoc_handle; + $this->sig = $sig; + $this->signed = $signed; + if ($invalidate_handle !== null) { + $this->invalidate_handle = $invalidate_handle; + } + } + + function fromQuery($query) + { + global $_Auth_OpenID_OpenID_Prefix; - case 'POST': - if ($args === null) { - $args = Auth_OpenID::fixArgs($_POST); + $required_keys = array('assoc_handle', 'sig', 'signed'); + + foreach ($required_keys as $k) { + if (!array_key_exists($_Auth_OpenID_OpenID_Prefix . $k, + $query)) { + return new Auth_OpenID_ServerError($query, + sprintf("%s request missing required parameter %s from \ + query", $this->mode, $k)); } - $mode = $args['openid.mode']; - switch ($mode) { + } - case 'associate': - return $this->associate($args); + $assoc_handle = $query[$_Auth_OpenID_OpenID_Prefix . 'assoc_handle']; + $sig = $query[$_Auth_OpenID_OpenID_Prefix . 'sig']; + $signed_list = $query[$_Auth_OpenID_OpenID_Prefix . 'signed']; + + $signed_list = explode(",", $signed_list); + $signed_pairs = array(); + + foreach ($signed_list as $field) { + if ($field == 'mode') { + // XXX KLUDGE HAX WEB PROTOCoL BR0KENNN + // + // openid.mode is currently check_authentication + // because that's the mode of this request. But the + // signature was made on something with a different + // openid.mode. + $value = "id_res"; + } else { + if (array_key_exists($_Auth_OpenID_OpenID_Prefix . $field, + $query)) { + $value = $query[$_Auth_OpenID_OpenID_Prefix . $field]; + } else { + return new Auth_OpenID_ServerError($query, + sprintf("Couldn't find signed field %r in query %s", + $field)); + } + } + $signed_pairs[] = array($field, $value); + } - case 'check_authentication': - return $this->checkAuthentication($args); + return new Auth_OpenID_CheckAuthRequest($assoc_handle, $sig, + $signed_pairs); + } - default: - $err = "Invalid openid.mode ($mode) for a POST request"; - return $this->postError($err); + function answer(&$signatory) + { + $is_valid = $signatory->verify($this->assoc_handle, $this->sig, + $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['is_valid'] = $is_valid ? "true" : "false"; + + if ($this->invalidate_handle) { + $assoc = $signatory->getAssociation($this->invalidate_handle, + false); + if (!$assoc) { + $response->fields['invalidate_handle'] = + $this->invalidate_handle; } + } + return $response; + } +} - default: - $err = "HTTP method $method is not part of OpenID"; - return array(Auth_OpenID_LOCAL_ERROR, $err); +class Auth_OpenID_AssociateRequest extends Auth_OpenID_Request { + var $mode = "associate"; + var $session_type = 'plaintext'; + var $assoc_type = 'HMAC-SHA1'; + + function fromQuery($query) + { + global $_Auth_OpenID_OpenID_Prefix; + + // FIXME: Missing dh_modulus and dh_gen options. + $obj = new Auth_OpenID_AssociateRequest(); + + $session_type = null; + + if (array_key_exists($_Auth_OpenID_OpenID_Prefix . 'session_type', + $query)) { + $session_type = $query[$_Auth_OpenID_OpenID_Prefix . + 'session_type']; } + + if ($session_type) { + $obj->session_type = $session_type; + + if ($session_type == 'DH-SHA1') { + if (array_key_exists($_Auth_OpenID_OpenID_Prefix . + 'dh_consumer_public', $query)) { + + # Auth_OpenID_getMathLib() + $lib =& Auth_OpenID_getMathLib(); + + $obj->pubkey = $lib->base64ToLong( + $query[$_Auth_OpenID_OpenID_Prefix . + 'dh_consumer_public']); + } else { + return new Auth_OpenID_ServerError($query, + "Public key for DH-SHA1 session not found in query"); + } + } + } + + return $obj; } - /** - * @access private - * - * @param object $request The Auth_OpenID_ServerRequest - * object representing this request. - * - * @param bool $authorized Whether the user making this request is - * capable of approving this authorization request. - */ - function getAuthResponse(&$request, $authorized) + function answer($assoc) { - $identity = $request->getIdentityURL(); - if (!isset($identity)) { - return $this->getError($request, 'No identity specified'); + $ml =& Auth_OpenID_getMathLib(); + $response = new Auth_OpenID_ServerResponse($this); + + $response->fields = array('expires_in' => $assoc->getExpiresIn(), + 'assoc_type' => 'HMAC-SHA1', + 'assoc_handle' => $assoc->handle); + + if ($this->session_type == 'DH-SHA1') { + // XXX - get dh_modulus and dh_gen + $dh = new Auth_OpenID_DiffieHellman(); + $mac_key = $dh->xorSecret($this->pubkey, $assoc->secret); + $response->fields['session_type'] = $this->session_type; + $response->fields['dh_server_public'] = + $ml->longToBase64($dh->public); + $response->fields['enc_mac_key'] = base64_encode($mac_key); + } else if ($this->session_type == 'plaintext') { + $response->fields['mac_key'] = base64_encode($assoc->secret); + } else { + // XXX - kablooie } - list($status, $info) = $this->_checkTrustRoot(&$request); - if (!$status) { - return $this->getError($request, $info); + return $response; + } +} + +class Auth_OpenID_CheckIDRequest extends Auth_OpenID_Request { + var $mode = "checkid_setup"; // or "checkid_immediate" + var $immediate = false; + var $trust_root = null; + + function make($query, $identity, $return_to, $trust_root = null, + $immediate = false, $assoc_handle = null) + { + if (!Auth_OpenID_TrustRoot::_parse($return_to)) { + return new Auth_OpenID_MalformedReturnURL($query, $return_to); + } + + return new Auth_OpenID_CheckIDRequest($identity, $return_to, + $trust_root, $immediate, + $assoc_handle); + } + + function Auth_OpenID_CheckIDRequest($identity, $return_to, + $trust_root = null, $immediate = false, + $assoc_handle = null) + { + $this->identity = $identity; + $this->return_to = $return_to; + $this->trust_root = $trust_root; + $this->assoc_handle = $assoc_handle; + + if ($immediate) { + $this->immediate = true; + $this->mode = "checkid_immediate"; } else { - $return_to = $info; + $this->immediate = false; + $this->mode = "checkid_setup"; } + } + + function fromQuery($query) + { + global $_Auth_OpenID_OpenID_Prefix; + + $mode = $query[$_Auth_OpenID_OpenID_Prefix . 'mode']; + $immediate = null; - if (!$authorized) { - return $this->_getAuthNotAuthorized(&$request, $return_to); + if ($mode == "checkid_immediate") { + $immediate = true; + $mode = "checkid_immediate"; } else { - return $this->_getAuthAuthorized(&$request, $return_to); - } - } - - /** - * Return whether the return_to URL matches the trust_root for - * this request. - * - * @access private - */ - function _checkTrustRoot(&$request) - { - $return_to = $request->getReturnTo(); - if (!isset($return_to)) { - return array(false, 'No return_to URL specified'); - } - - $trust_root = $request->getTrustRoot(); - if (isset($trust_root) && - !Auth_OpenID_TrustRoot::match($trust_root, $return_to)) { - return array(false, 'Trust root does not match'); - } - return array(true, $return_to); - } - - /** - * @access private - */ - function _getAuthNotAuthorized(&$request, $return_to) - { - $mode = $request->getMode(); - switch ($mode) { - case 'checkid_immediate': - // Build a URL that is just the URL that came here - // with the mode changed from checkid_immediate to - // checkid_setup. - $args = $request->args; - $args['openid.mode'] = 'checkid_setup'; - $setup_url = Auth_OpenID::appendArgs($this->server_url, $args); - - // Return to the consumer, instructing it that the user - // needs to do something in order to verify his identity. - $rargs = array( - 'openid.mode' => 'id_res', - 'openid.user_setup_url' => $setup_url - ); - - $redir_url = Auth_OpenID::appendArgs($return_to, $rargs); - return array(Auth_OpenID_REDIRECT, $redir_url); - - case 'checkid_setup': - // Return to the application indicating that the user - // needs to authenticate. - return array(Auth_OpenID_DO_AUTH, &$request); - - default: - $err = sprintf('invalid openid.mode (%s) for GET requests', $mode); - return $this->getError($request, $err); - } - } - - /** - * @access private - */ - function _getAuthAuthorized(&$request, $return_to) - { - $mode = $request->getMode(); - switch ($mode) { - case 'checkid_immediate': - case 'checkid_setup': - break; - default: - $err = sprintf('invalid openid.mode (%s) for GET requests', $mode); - return $this->getError($request, $err); - } - - $reply = array('openid.mode' => 'id_res', - 'openid.return_to' => $return_to, - 'openid.identity' => $request->getIdentityURL() - ); - - $assoc = null; - $assoc_handle = @$request->args['openid.assoc_handle']; - if (isset($assoc_handle)) { - $key = $this->_normal_key; - $assoc = $this->store->getAssociation($key, $assoc_handle); - - // fall back to dumb mode if assoc_handle not found, - // and send the consumer an invalidate_handle message - if (!isset($assoc) || $assoc->getExpiresIn() <= 0) { - $assoc = null; - $this->store->removeAssociation($key, $assoc_handle); - $reply['openid.invalidate_handle'] = $assoc_handle; + $immediate = false; + $mode = "checkid_setup"; + } + + $required = array('identity', + 'return_to'); + + $optional = array('trust_root', + 'assoc_handle'); + + $values = array(); + + foreach ($required as $field) { + if (array_key_exists($_Auth_OpenID_OpenID_Prefix . $field, + $query)) { + $value = $query[$_Auth_OpenID_OpenID_Prefix . $field]; + } else { + return new Auth_OpenID_ServerError($query, + sprintf("Missing required field %s from request", + $field)); } + $values[$field] = $value; } - // Use dumb mode if there is no association. - if ($assoc === null) { - $assoc = $this->createAssociation('HMAC-SHA1'); - $this->store->storeAssociation($this->_dumb_key, $assoc); + foreach ($optional as $field) { + $value = null; + if (array_key_exists($_Auth_OpenID_OpenID_Prefix . $field, + $query)) { + $value = $query[$_Auth_OpenID_OpenID_Prefix. $field]; + } + if ($value) { + $values[$field] = $value; + } } - $reply['openid.assoc_handle'] = $assoc->handle; - $signed_fields = array('mode', 'identity', 'return_to'); - $assoc->addSignature($signed_fields, &$reply); - $redir_url = Auth_OpenID::appendArgs($return_to, $reply); - return array(Auth_OpenID_REDIRECT, $redir_url); + if (!Auth_OpenID_TrustRoot::_parse($values['return_to'])) { + return new Auth_OpenID_MalformedReturnURL($query, + $values['return_to']); + } + + $obj = Auth_OpenID_CheckIDRequest::make($query, + $values['identity'], + $values['return_to'], + Auth_OpenID::arrayGet($values, + 'trust_root', null), + $immediate); + + if (Auth_OpenID::arrayGet($values, 'assoc_handle')) { + $obj->assoc_handle = $values['assoc_handle']; + } + + return $obj; } - /** - * Perform an openid.mode=associate query - * - * @access private - */ - function associate($query) + function trustRootValid() { - $reply = array(); + if (!$this->trust_root) { + return true; + } - $assoc_type = @$query['openid.assoc_type']; - if (!isset($assoc_type)) { - $assoc_type = 'HMAC-SHA1'; + $tr = Auth_OpenID_TrustRoot::_parse($this->trust_root); + if ($tr === false) { + return new Auth_OpenID_MalformedTrustRoot(null, $this->trust_root); } - $assoc = $this->createAssociation($assoc_type); - if (!isset($assoc)) { - $fmt = 'unable to create an association for type %s'; - return $this->postError(sprinft($fmt, $assoc_type)); + return Auth_OpenID_TrustRoot::match($this->trust_root, + $this->return_to); + } + + function answer($allow, $server_url = null) + { + if ($allow || $this->immediate) { + $mode = 'id_res'; + } else { + $mode = 'cancel'; } - $this->store->storeAssociation($this->_normal_key, $assoc); + $response = new Auth_OpenID_CheckIDResponse($this, $mode); - if (isset($assoc_type)) { - $reply['assoc_type'] = $assoc_type; + if ($allow) { + $response->fields['identity'] = $this->identity; + $response->fields['return_to'] = $this->return_to; + if (!$this->trustRootValid()) { + return new Auth_OpenID_UntrustedReturnURL($this->return_to, + $this->trust_root); + } + } else { + $response->signed = array(); + if ($this->immediate) { + if (!$server_url) { + return new Auth_OpenID_ServerError(null, + 'setup_url is required for $allow=false \ + in immediate mode.'); + } + + $setup_request =& new Auth_OpenID_CheckIDRequest( + $this->identity, + $this->return_to, + $this->trust_root, + false, + $this->assoc_handle); + + $setup_url = $setup_request->encodeToURL($server_url); + + $response->fields['user_setup_url'] = $setup_url; + } } - $reply['assoc_handle'] = $assoc->handle; - $reply['expires_in'] = strval($assoc->getExpiresIn()); - if (defined('Auth_OpenID_NO_MATH_SUPPORT')) { - $session_type = null; + return $response; + } + + function encodeToURL($server_url) + { + global $_Auth_OpenID_OpenID_Prefix; + + // 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, + 'return_to' => $this->return_to); + + if ($this->trust_root) { + $q['trust_root'] = $this->trust_root; + } + + if ($this->assoc_handle) { + $q['assoc_handle'] = $this->assoc_handle; + } + + $_q = array(); + + foreach ($q as $k => $v) { + $_q[$_Auth_OpenID_OpenID_Prefix . $k] = $v; + } + + return Auth_OpenID::appendArgs($server_url, $_q); + } + + function getCancelURL() + { + global $_Auth_OpenID_OpenID_Prefix; + + if ($this->immediate) { + return new Auth_OpenID_ServerError(null, + "Cancel is not an appropriate \ + response to immediate mode \ + requests."); + } + + return Auth_OpenID::appendArgs($this->return_to, + array($_Auth_OpenID_OpenID_Prefix . 'mode' => + 'cancel')); + } +} + +class Auth_OpenID_ServerResponse { + + function Auth_OpenID_ServerResponse($request) + { + $this->request = $request; + $this->fields = array(); + } + + function whichEncoding() + { + global $_Auth_OpenID_Encode_Kvform, + $_Auth_OpenID_Request_Modes, + $_Auth_OpenID_Encode_Url; + + if (in_array($this->request->mode, $_Auth_OpenID_Request_Modes)) { + return $_Auth_OpenID_Encode_Url; } else { - $session_type = @$query['openid.session_type']; + return $_Auth_OpenID_Encode_Kvform; } + } + + function encodeToURL() + { + global $_Auth_OpenID_OpenID_Prefix; + + $fields = array(); - switch ($session_type) { - case 'DH-SHA1': - $sess_reply = Auth_OpenID_DiffieHellman:: - serverAssociate($query, $assoc->secret); - break; - case null: - $sess_reply = array('mac_key' => base64_encode($assoc->secret)); - break; - default: - $sess_reply = false; + foreach ($this->fields as $k => $v) { + $fields[$_Auth_OpenID_OpenID_Prefix . $k] = $v; } - if ($sess_reply === false) { - $msg = "Association session (type $session_type) failed"; - return $this->postError($msg); + return Auth_OpenID::appendArgs($this->request->return_to, $fields); + } + + function encodeToKVForm() + { + return Auth_OpenID_KVForm::fromArray($this->fields); + } +} + +class Auth_OpenID_CheckIDResponse extends Auth_OpenID_ServerResponse { + + function Auth_OpenID_CheckIDResponse(&$request, $mode = 'id_res') + { + parent::Auth_OpenID_ServerResponse(&$request); + $this->fields['mode'] = $mode; + $this->signed = array(); + + if ($mode == 'id_res') { + array_push($this->signed, 'mode', 'identity', 'return_to'); } + } - $reply = array_merge($reply, $sess_reply); - $reply_kv = Auth_OpenID_KVForm::fromArray($reply); - return array(Auth_OpenID_REMOTE_OK, $reply_kv); + function addField($namespace, $key, $value, $signed = true) + { + if ($namespace) { + $key = sprintf('%s.%s', $namespace, $key); + } + $this->fields[$key] = $value; + if ($signed && !in_array($key, $this->signed)) { + $this->signed[] = $key; + } } - /** - * Perform an openid.mode=check_authentication request - * - * @access private - */ - function checkAuthentication($args) + function addFields($namespace, $fields, $signed = true) { - $handle = $args['openid.assoc_handle']; - if (!isset($handle)) { - return $this->postError('Missing openid.assoc_handle'); + foreach ($fields as $k => $v) { + $this->addField($namespace, $k, $v, $signed); } + } - $store =& $this->store; - $assoc = $store->getAssociation($this->_dumb_key, $handle); - $reply = array('is_valid' => 'false'); - if ($assoc !== null && $assoc->getExpiresIn() > 0) { - $signed = $args['openid.signed']; - if (!isset($signed)) { - return $this->postError('Missing openid.signed'); - } + function update($namespace, $other) + { + $namespaced_fields = array(); - $sig = $args['openid.sig']; - if (!isset($sig)) { - return $this->postError('Missing openid.sig'); - } + foreach ($other->fields as $k => $v) { + $name = sprintf('%s.%s', $namespace, $k); - $to_verify = $args; - $to_verify['openid.mode'] = 'id_res'; - $fields = explode(',', trim($signed)); - $tv_sig = $assoc->signDict($fields, $to_verify); - - if ($tv_sig == $sig) { - $normal_key = $this->_normal_key; - $store->removeAssociation($normal_key, $assoc->handle); - $reply['is_valid'] = 'true'; - - $inv_handle = @$args['openid.invalidate_handle']; - if (isset($inv_handle)) { - $assoc = $store->getAssociation($normal_key, $inv_handle); - if (!isset($assoc)) { - $reply['invalidate_handle'] = $inv_handle; - } - } - } - } elseif ($assoc !== null) { - $store->removeAssociation($this->_dumb_key, $assoc_handle); + $namespaced_fields[$name] = $v; } - $kv = Auth_OpenID_KVForm::fromArray($reply); - return array(Auth_OpenID_REMOTE_OK, $kv); + $this->fields = array_merge($this->fields, $namespaced_fields); + $this->signed = array_merge($this->signed, $other->signed); } +} + +class Auth_OpenID_WebResponse { + var $code = AUTH_OPENID_HTTP_OK; + var $body = ""; - /** - * Create a new association and store it - * - * @access private - */ - function createAssociation($assoc_type) + function Auth_OpenID_WebResponse($code = null, $headers = null, + $body = null) { - if ($assoc_type == 'HMAC-SHA1') { - $secret = Auth_OpenID_CryptUtil::getBytes(20); + if ($code) { + $this->code = $code; + } + + if ($headers !== null) { + $this->headers = $headers; } else { - // XXX: log + $this->headers = array(); + } + + if ($body !== null) { + $this->body = $body; + } + } +} + +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'; + + function Auth_OpenID_Signatory(&$store) + { + // assert store is not None + $this->store =& $store; + } + + function verify($assoc_handle, $sig, $signed_pairs) + { + $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; } + $expected_sig = base64_encode($assoc->sign($signed_pairs)); + + return $sig == $expected_sig; + } + + function sign($response) + { + $signed_response = $response; + $assoc_handle = $response->request->assoc_handle; + + if ($assoc_handle) { + // normal mode + $assoc = $this->getAssociation($assoc_handle, false); + if (!$assoc) { + // fall back to dumb mode + $signed_response->fields['invalidate_handle'] = $assoc_handle; + $assoc = $this->createAssociation(true); + } + } else { + // dumb mode. + $assoc = $this->createAssociation(true); + } + + $signed_response->fields['assoc_handle'] = $assoc->handle; + $assoc->addSignature($signed_response->signed, + $signed_response->fields, ''); + return $signed_response; + } + + function createAssociation($dumb = true, $assoc_type = 'HMAC-SHA1') + { + $secret = Auth_OpenID_CryptUtil::getBytes(20); $uniq = base64_encode(Auth_OpenID_CryptUtil::getBytes(4)); - $handle = sprintf('{%s}{%x}{%s}', $assoc_type, time(), $uniq); + $handle = sprintf('{%s}{%x}{%s}', $assoc_type, intval(time()), $uniq); - $ltime = $this->association_lifetime; - $assoc = Auth_OpenID_Association:: - fromExpiresIn($ltime, $handle, $secret, $assoc_type); + $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; } - /** - * Return an error response for GET requests - * - * @access private - */ - function getError($request, $msg) - { - $args = $request->args; - $return_to = @$args['openid.return_to']; - if (isset($return_to)) { - $err = array( - 'openid.mode' => 'error', - 'openid.error' => $msg - ); - $redir_url = Auth_OpenID::appendArgs($return_to, $err); - return array(Auth_OpenID_REDIRECT, $redir_url); + function getAssociation($assoc_handle, $dumb) + { + if ($assoc_handle === null) { + return new Auth_OpenID_ServerError(null, + "assoc_handle must not be null"); + } + + if ($dumb) { + $key = $this->dumb_key; } else { - foreach (array_keys($args) as $k) { - if (preg_match('/^openid\./', $k)) { - return array(Auth_OpenID_LOCAL_ERROR, $msg); - } + $key = $this->normal_key; + } + + $assoc = $this->store->getAssociation($key, $assoc_handle); + + if (($assoc !== null) && ($assoc->getExpiresIn() <= 0)) { + $this->store->removeAssociation($key, $assoc_handle); + $assoc = null; + } + + return $assoc; + } + + function invalidate($assoc_handle, $dumb) + { + if ($dumb) { + $key = $this->dumb_key; + } else { + $key = $this->normal_key; + } + $this->store->removeAssociation($key, $assoc_handle); + } +} + +class Auth_OpenID_Encoder { + + var $responseFactory = 'Auth_OpenID_WebResponse'; + + function encode(&$response) + { + global $_Auth_OpenID_Encode_Kvform, + $_Auth_OpenID_Encode_Url; + + $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 { + return new Auth_OpenID_EncodingError(&$response); + } + return $wr; + } +} + +function needsSigning($response) +{ + return (in_array($response->request->mode, array('checkid_setup', + 'checkid_immediate')) && + $response->signed); +} + +class Auth_OpenID_SigningEncoder extends Auth_OpenID_Encoder { + + function Auth_OpenID_SigningEncoder(&$signatory) + { + $this->signatory =& $signatory; + } + + 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') && + needsSigning($response)) { + + if (!$this->signatory) { + return new Auth_OpenID_ServerError(null, + "Must have a store to sign request"); + } + if (array_key_exists('sig', $response->fields)) { + return new Auth_OpenID_AlreadySigned($response); + } + $response = $this->signatory->sign($response); + } + return parent::encode($response); + } +} + +class Auth_OpenID_Decoder { + + function Auth_OpenID_Decoder() + { + global $_Auth_OpenID_OpenID_Prefix; + $this->prefix = $_Auth_OpenID_OpenID_Prefix; + + $this->handlers = array( + 'checkid_setup' => 'Auth_OpenID_CheckIDRequest', + 'checkid_immediate' => 'Auth_OpenID_CheckIDRequest', + 'check_authentication' => 'Auth_OpenID_CheckAuthRequest', + 'associate' => 'Auth_OpenID_AssociateRequest' + ); + } - return array(Auth_OpenID_DO_ABOUT, null); + function decode($query) + { + if (!$query) { + return null; + } + + $myquery = array(); + + foreach ($query as $k => $v) { + if (strpos($k, $this->prefix) === 0) { + $myquery[$k] = $v; + } } + + if (!$myquery) { + return null; + } + + $mode = Auth_OpenID::arrayGet($myquery, $this->prefix . 'mode'); + if (!$mode) { + return new Auth_OpenID_ServerError($query, + sprintf("No %smode found in query", $this->prefix)); + } + + $handlerCls = Auth_OpenID::arrayGet($this->handlers, $mode, + $this->defaultDecoder($query)); + + if (!is_a($handlerCls, 'Auth_OpenID_ServerError')) { + return call_user_func_array(array($handlerCls, 'fromQuery'), + array($query)); + } else { + return $handlerCls; + } + } + + function defaultDecoder($query) + { + $mode = $query[$this->prefix . 'mode']; + return new Auth_OpenID_ServerError($query, + sprintf("No decoder for mode %s", $mode)); + } +} + +class Auth_OpenID_EncodingError { + function Auth_OpenID_EncodingError(&$response) + { + $this->response =& $response; + } +} + +class Auth_OpenID_AlreadySigned extends Auth_OpenID_EncodingError { + // This response is already signed. +} + +class Auth_OpenID_UntrustedReturnURL extends Auth_OpenID_ServerError { + function Auth_OpenID_UntrustedReturnURL($return_to, $trust_root) + { + $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); + } +} + +/** + * An object that implements the OpenID protocol for a single URL. + * + * Use this object by calling getOpenIDResponse when you get any + * request for the server URL. + * + * @package OpenID + */ +class Auth_OpenID_Server { + function Auth_OpenID_Server(&$store) + { + $this->store =& $store; + $this->signatory =& new Auth_OpenID_Signatory($this->store); + $this->encoder =& new Auth_OpenID_Encoder($this->signatory); + $this->decoder =& new Auth_OpenID_Decoder(); + } + + function handleRequest($request) + { + if (method_exists($this, "openid_" . $request->mode)) { + $handler = "openid_" . $request->mode; + return $handler($request); + } + return null; + } + + function openid_check_authentication(&$request) + { + return $request->answer($this->signatory); + } + + function openid_associate(&$request) + { + $assoc = $this->signatory->createAssociation(false); + return $request->answer($assoc); + } + + function encodeResponse(&$response) + { + return $this->encoder->encode($response); } - /** - * Return an error response for POST requests - * - * @access private - */ - function postError($msg) + function decodeRequest(&$query) { - $kv = Auth_OpenID_KVForm::fromArray(array('error' => $msg)); - return array(Auth_OpenID_REMOTE_ERROR, $kv); + return $this->decoder->decode($query); } } -?>
\ No newline at end of file +?> diff --git a/Auth/OpenID/ServerRequest.php b/Auth/OpenID/ServerRequest.php index ff34b6b..0072894 100644 --- a/Auth/OpenID/ServerRequest.php +++ b/Auth/OpenID/ServerRequest.php @@ -28,124 +28,9 @@ require_once "Auth/OpenID.php"; * @package OpenID */ class Auth_OpenID_ServerRequest { - /** - * The arguments for this request - */ - var $args; - - /** - * The URL of the server for this request - */ - var $server_url; - - /** - * Constructor - * - * @internal This is private because the library user should not - * have to make instances of this class. - * - * @access private - * - * @param string $server_url The openid.server URL for the server - * that goes with this request. - * - * @param array $args The query arguments for this request - */ - function Auth_OpenID_ServerRequest($server_url, $args) - { - $this->server_url = $server_url; - $this->args = $args; - } - - /** - * @access private - */ - function getMode() - { - return $this->args['openid.mode']; - } - - /** - * Get the identity URL that is being checked - */ - function getIdentityURL() - { - return @$this->args['openid.identity']; - } - - /** - * Get the return_to URL for the consumer that initiated this request. - * - * @return string $return_to The return_to URL for the consumer - */ - function getReturnTo() - { - return @$this->args['openid.return_to']; - } - - /** - * Get a cancel response for this URL - * - * @return array $response The status code and data - */ - function cancel() + function Auth_OpenID_ServerRequest() { - return array(Auth_OpenID_REDIRECT, $this->getCancelURL()); - } - - /** - * Return a cancel URL for this request - */ - function getCancelURL() - { - $cancel_args = array('openid.mode' => 'cancel'); - $return_to = $this->args['openid.return_to']; - return Auth_OpenID::appendArgs($return_to, $cancel_args); - } - - /** - * Get a URL that will initiate this request again. - */ - function getRetryURL() - { - return Auth_OpenID::appendArgs($this->server_url, $this->args); - } - - /** - * Return the trust_root for this request - */ - function getTrustRoot() - { - if (isset($this->args['openid.trust_root'])) { - return $this->args['openid.trust_root']; - } else { - return @$this->args['openid.return_to']; - } - } - - /** - * Attempt to authenticate again, given a server and - * authentication checking function. - * - * @param object $server An instance of {@link Auth_OpenID_Server} - * - * @param mixed $is_authorized The callback to use to determine - * whether the current user can authorize this request. - */ - function retry(&$server, $is_authorized) - { - $trust_root = $this->getTrustRoot(); - $identity_url = $this->getIdentityURL(); - - // If there is no return_to or trust_root or there is no - // identity_url, then it's impossible to continue. - if (isset($identity_url) && isset($trust_root) && $is_authorized) { - $authorized = $is_authorized($identity_url, $trust_root); - } else { - $authorized = false; - } - - return $server->getAuthResponse(&$this, $authorized); + $this->mode = null; } } diff --git a/Tests/Auth/OpenID/Server.php b/Tests/Auth/OpenID/Server.php index 9bbe29f..393dc4c 100644 --- a/Tests/Auth/OpenID/Server.php +++ b/Tests/Auth/OpenID/Server.php @@ -7,297 +7,1240 @@ require_once "PHPUnit.php"; require_once "Tests/Auth/OpenID/MemStore.php"; require_once "Auth/OpenID.php"; +require_once "Auth/OpenID/DiffieHellman.php"; require_once "Auth/OpenID/Server.php"; +function arrayToString($arr) +{ + $s = "Array("; + + $parts = array(); + foreach ($arr as $k => $v) { + if (is_array($v)) { + $v = arrayToString($v); + } + $parts[] = sprintf("%s => %s", $k, $v); + } + + $s .= implode(", ", $parts); + $s .= ")"; + + return $s; +} + function _Auth_OpenID_NotAuthorized() { return false; } -class Tests_Auth_OpenID_Server extends PHPUnit_TestCase { +class Tests_Auth_OpenID_Test_ServerError extends PHPUnit_TestCase { + function test_browserWithReturnTo() + { + $return_to = "http://rp.unittest/consumer"; + // will be a ProtocolError raised by Decode or CheckIDRequest.answer + $args = array( + 'openid.mode' => 'monkeydance', + 'openid.identity' => 'http://wagu.unittest/', + 'openid.return_to' => $return_to); + $e = new Auth_OpenID_ServerError($args, "plucky"); + $this->assertTrue($e->hasReturnTo()); + $expected_args = array( + 'openid.mode' => 'error', + 'error' => 'plucky'); + + $encoded = $e->encodeToURL(); + if (_isError($encoded)) { + $this->fail($encoded->toString()); + return; + } + + list($rt_base, $_result_args) = explode("?", $e->encodeToURL(), 2); + $result_args = array(); + parse_str($_result_args, $result_args); + $result_args = Auth_OpenID::fixArgs($result_args); + + $this->assertEquals($result_args, $expected_args); + } + + function test_noReturnTo() + { + // will be a ProtocolError raised by Decode or CheckIDRequest.answer + $args = array( + 'openid.mode' => 'zebradance', + 'openid.identity' => 'http://wagu.unittest/'); + + $e = new Auth_OpenID_ServerError($args, "waffles"); + $this->assertFalse($e->hasReturnTo()); + $expected = "error:waffles\nmode:error\n"; + $this->assertEquals($e->encodeToKVForm(), $expected); + } +} + +class Tests_Auth_OpenID_Test_Decode extends PHPUnit_TestCase { function setUp() { - $this->sv_url = 'http://id.server.url/'; - $this->id_url = 'http://foo.com/'; - $this->rt_url = 'http://return.to/rt'; - $this->tr_url = 'http://return.to/'; - $this->noauth = '_Auth_OpenID_NotAuthorized'; + $this->id_url = "http://decoder.am.unittest/"; + $this->rt_url = "http://rp.unittest/foobot/?qux=zam"; + $this->tr_url = "http://rp.unittest/"; + $this->assoc_handle = "{assoc}{handle}"; + $this->decoder = new Auth_OpenID_Decoder(); + } - $this->store = new Tests_Auth_OpenID_MemStore(); - $this->server =& new Auth_OpenID_Server($this->sv_url, &$this->store); + function test_none() + { + $args = array(); + $r = $this->decoder->decode($args); + $this->assertEquals($r, null); } - function _parseRedirResp($ret) + function test_irrelevant() { - list($status, $redir) = $ret; - if ($status != Auth_OpenID_REDIRECT) { - $this->fail("Bad status: $status"); - return false; + $args = array( + 'pony' => 'spotted', + 'sreg.mutant_power' => 'decaffinator'); + + $r = $this->decoder->decode($args); + + $this->assertTrue($r === null); + } + + function test_bad() + { + $args = array( + 'openid.mode' => 'twos-compliment', + 'openid.pants' => 'zippered'); + + // Be sure that decoding the args returns an error. + $result = $this->decoder->decode($args); + + $this->assertTrue(_isError($result)); + } + + function test_checkidImmediate() + { + $args = array( + 'openid.mode' => 'checkid_immediate', + 'openid.identity' => $this->id_url, + 'openid.assoc_handle' => $this->assoc_handle, + 'openid.return_to' => $this->rt_url, + 'openid.trust_root' => $this->tr_url, + # should be ignored + 'openid.some.extension' => 'junk'); + + $r = $this->decoder->decode($args); + $this->assertTrue(is_a($r, 'Auth_OpenID_CheckIDRequest')); + $this->assertEquals($r->mode, "checkid_immediate"); + $this->assertEquals($r->immediate, true); + $this->assertEquals($r->identity, $this->id_url); + $this->assertEquals($r->trust_root, $this->tr_url); + $this->assertEquals($r->return_to, $this->rt_url); + $this->assertEquals($r->assoc_handle, $this->assoc_handle); + } + + function test_checkidSetup() + { + $args = array( + 'openid.mode' => 'checkid_setup', + 'openid.identity' => $this->id_url, + 'openid.assoc_handle' => $this->assoc_handle, + 'openid.return_to' => $this->rt_url, + 'openid.trust_root' => $this->tr_url); + + $r = $this->decoder->decode($args); + $this->assertTrue(is_a($r, 'Auth_OpenID_CheckIDRequest')); + $this->assertEquals($r->mode, "checkid_setup"); + $this->assertEquals($r->immediate, false); + $this->assertEquals($r->identity, $this->id_url); + $this->assertEquals($r->trust_root, $this->tr_url); + $this->assertEquals($r->return_to, $this->rt_url); + } + + function test_checkidSetupNoIdentity() + { + $args = array( + 'openid.mode' => 'checkid_setup', + 'openid.assoc_handle' => $this->assoc_handle, + 'openid.return_to' => $this->rt_url, + 'openid.trust_root' => $this->tr_url); + + $result = $this->decoder->decode($args); + if (_isError($result)) { + $this->assertTrue($result->query); + } else { + $this->fail(sprintf("Expected Auth_OpenID_Error, instead " . + "returned with %s", gettype($result))); + } + } + + function test_checkidSetupNoReturn() + { + $args = array( + 'openid.mode' => 'checkid_setup', + 'openid.identity' => $this->id_url, + 'openid.assoc_handle' => $this->assoc_handle, + 'openid.trust_root' => $this->tr_url); + + $result = $this->decoder->decode($args); + if (!_isError($result)) { + $this->fail("Expected Auth_OpenID_Error"); } + } - list($base, $query_str) = explode('?', $redir, 2); + function test_checkidSetupBadReturn() + { + $args = array( + 'openid.mode' => 'checkid_setup', + 'openid.identity' => $this->id_url, + 'openid.assoc_handle' => $this->assoc_handle, + 'openid.return_to' => 'not a url'); - $query = array(); - parse_str($query_str, $query); - $query = Auth_OpenID::fixArgs($query); - return array($base, $query); + $result = $this->decoder->decode($args);; + if (_isError($result)) { + $this->assertTrue($result->query); + } else { + $this->fail(sprintf("Expected ProtocolError, instead " . + "returned with %s", gettype($result))); + } + } + + function test_checkAuth() + { + $args = array( + 'openid.mode' => 'check_authentication', + 'openid.assoc_handle' => '{dumb}{handle}', + 'openid.sig' => 'sigblob', + 'openid.signed' => 'foo,bar,mode', + 'openid.foo' => 'signedval1', + 'openid.bar' => 'signedval2', + 'openid.baz' => 'unsigned'); + + $r = $this->decoder->decode($args); + $this->assertTrue(is_a($r, 'Auth_OpenID_CheckAuthRequest')); + $this->assertEquals($r->mode, 'check_authentication'); + $this->assertEquals($r->sig, 'sigblob'); + $this->assertEquals($r->signed, array( + array('foo', 'signedval1'), + array('bar', 'signedval2'), + array('mode', 'id_res'))); + + // XXX: and invalidate_handle, which is optional + // XXX: test error cases (missing required fields, + // missing fields that are in the signed list). } - function test_getWithReturnToError() + function test_associateDH() { $args = array( - 'openid.mode' => 'monkeydance', - 'openid.identity' => $this->id_url, - 'openid.return_to' => $this->rt_url, - ); + 'openid.mode' => 'associate', + 'openid.session_type' => 'DH-SHA1', + 'openid.dh_consumer_public' => "Rzup9265tw=="); - $ret = $this->server->getOpenIDResponse($this->noauth, 'GET', $args); + $r = $this->decoder->decode($args); + $this->assertTrue(is_a($r, 'Auth_OpenID_AssociateRequest')); + $this->assertEquals($r->mode, "associate"); + $this->assertEquals($r->session_type, "DH-SHA1"); + $this->assertEquals($r->assoc_type, "HMAC-SHA1"); + $this->assertTrue($r->pubkey); + } - list($rt_base, $resultArgs) = $this->_parseRedirResp($ret); + function test_associateDHMissingKey() + { + $args = array( + 'openid.mode' => 'associate', + 'openid.session_type' => 'DH-SHA1'); - $this->assertEquals($this->rt_url, $rt_base); - $this->assertEquals('error', $resultArgs['openid.mode']); - if (!array_key_exists('openid.error', $resultArgs)) { - $dump = var_export($resultArgs, true); - $msg = sprintf("no openid.error in %s", $dump); - $this->fail($msg); + // Using DH-SHA1 without supplying dh_consumer_public is an error. + $result = $this->decoder->decode($args); + if (!_isError($result)) { + $this->fail(sprintf("Expected Auth_OpenID_Error, got %s", + gettype($result))); } } - function test_getBadArgsError() + function test_associatePlain() + { + $args = array('openid.mode' => 'associate'); + + $r = $this->decoder->decode($args); + $this->assertTrue(is_a($r, 'Auth_OpenID_AssociateRequest')); + $this->assertEquals($r->mode, "associate"); + $this->assertEquals($r->session_type, "plaintext"); + $this->assertEquals($r->assoc_type, "HMAC-SHA1"); + } + + function test_nomode() { $args = array( - 'openid.mode' => 'zebradance', - 'openid.identity' => $this->id_url, - ); + 'openid.session_type' => 'DH-SHA1', + 'openid.dh_consumer_public' => "my public keeey"); - list($status, $info) = $this->server->getOpenIDResponse( - $this->noauth, 'GET', $args); + $result = $this->decoder->decode($args); + if (!_isError($result)) { + $this->fail(sprintf("Expected Auth_OpenID_Error", + gettype($result))); + } + } +} - $this->assertEquals(Auth_OpenID_LOCAL_ERROR, $status); - $this->assertTrue($info); +class Tests_Auth_OpenID_Test_Encode extends PHPUnit_TestCase { + function setUp() + { + $this->encoder = new Auth_OpenID_Encoder(); + $this->encode = $this->encoder; } - function test_getNoArgsError() + function test_id_res() { - list($status, $info) = $this->server->getOpenIDResponse( - $this->noauth, 'GET', array()); + $request = new Auth_OpenID_CheckIDRequest( + 'http://bombom.unittest/', + 'http://burr.unittest/', + 'http://burr.unittest/999', + false); + + $response = new Auth_OpenID_CheckIDResponse($request); + $response->fields = array( + 'mode' => 'id_res', + 'identity' => $request->identity, + 'return_to' => $request->return_to); + + $webresponse = $this->encoder->encode($response); + $this->assertEquals($webresponse->code, AUTH_OPENID_HTTP_REDIRECT); + $this->assertTrue(array_key_exists('location', + $webresponse->headers)); - $this->assertEquals(Auth_OpenID_DO_ABOUT, $status); + $location = $webresponse->headers['location']; + $this->assertTrue(strpos($location, $request->return_to) === 0); + // "%s does not start with %s" % ($location, + // $request->return_to)); + + $parsed = parse_url($location); + $query = array(); + parse_str($parsed['query'], $query); + $query = Auth_OpenID::fixArgs($query); + + $expected = array(); + + foreach ($response->fields as $k => $v) { + $expected['openid.' . $k] = $v; + } + + $this->assertEquals($query, $expected); + } + + function test_cancel() + { + $request = new Auth_OpenID_CheckIDRequest( + 'http://bombom.unittest/', + 'http://burr.unittest/', + 'http://burr.unittest/999', + false); + + $response = new Auth_OpenID_CheckIDResponse($request); + $response->fields = array('mode' => 'cancel'); + + $webresponse = $this->encoder->encode($response); + $this->assertEquals($webresponse->code, AUTH_OPENID_HTTP_REDIRECT); + $this->assertTrue(array_key_exists('location', $webresponse->headers)); + } + + function test_assocReply() + { + $request = new Auth_OpenID_AssociateRequest(); + $response = new Auth_OpenID_ServerResponse($request); + $response->fields = array('assoc_handle' => "every-zig"); + $webresponse = $this->encoder->encode($response); + $body = "assoc_handle:every-zig\n"; + $this->assertEquals($webresponse->code, AUTH_OPENID_HTTP_OK); + $this->assertEquals($webresponse->headers, array()); + $this->assertEquals($webresponse->body, $body); + } + + function test_checkauthReply() + { + $request = new Auth_OpenID_CheckAuthRequest('a_sock_monkey', + 'siggggg', + array()); + $response = new Auth_OpenID_ServerResponse($request); + $response->fields = array( + 'is_valid' => 'true', + 'invalidate_handle' => 'xXxX:xXXx'); + + $body = "invalidate_handle:xXxX:xXXx\nis_valid:true\n"; + $webresponse = $this->encoder->encode($response); + $this->assertEquals($webresponse->code, AUTH_OPENID_HTTP_OK); + $this->assertEquals($webresponse->headers, array()); + $this->assertEquals($webresponse->body, $body); + } + + function test_unencodableError() + { + $args = array('openid.identity' => 'http://limu.unittest/'); + + $e = new Auth_OpenID_ServerError($args, "wet paint"); + + $result = $this->encoder->encode($e); + if (!_isError($result, 'Auth_OpenID_EncodingError')) { + $this->fail(sprintf("Expected Auth_OpenID_ServerError, got %s", + gettype($result))); + } } - function test_postError() + function test_encodableError() { $args = array( - 'openid.mode' => 'pandadance', - 'openid.identity' => $this->id_url, - ); + 'openid.mode' => 'associate', + 'openid.identity' => 'http://limu.unittest/'); + + $body="error:snoot\nmode:error\n"; + $err = new Auth_OpenID_ServerError($args, "snoot"); + $webresponse = $this->encoder->encode($err); + $this->assertEquals($webresponse->code, AUTH_OPENID_HTTP_ERROR); + $this->assertEquals($webresponse->headers, array()); + $this->assertEquals($webresponse->body, $body); + } +} + +class Tests_Auth_OpenID_SigningEncode extends PHPUnit_TestCase { + function setUp() + { + // Use filestore here instead of memstore + $this->store = new Tests_Auth_OpenID_MemStore(); + + $this->request = new Auth_OpenID_CheckIDRequest( + 'http://bombom.unittest/', + 'http://burr.unittest/', + 'http://burr.unittest/999', + false); + + $this->response = new Auth_OpenID_CheckIDResponse($this->request); + $this->response->fields = array( + 'mode' => 'id_res', + 'identity' => $this->request->identity, + 'return_to' => $this->request->return_to); + + $this->signatory = new Auth_OpenID_Signatory($this->store); + $this->dumb_key = $this->signatory->dumb_key; + $this->normal_key = $this->signatory->normal_key; - list($status, $info) = $this->server->getOpenIDResponse( - $this->noauth, 'POST', $args); + $this->encoder = new Auth_OpenID_SigningEncoder($this->signatory); + } + + function test_idres() + { + $assoc_handle = '{bicycle}{shed}'; + $this->store->storeAssociation( + $this->normal_key, + Auth_OpenID_Association::fromExpiresIn(60, $assoc_handle, + 'sekrit', 'HMAC-SHA1')); + $this->request->assoc_handle = $assoc_handle; + $webresponse = $this->encoder->encode($this->response); + $this->assertEquals($webresponse->code, AUTH_OPENID_HTTP_REDIRECT); + $this->assertTrue(array_key_exists('location', + $webresponse->headers)); + + $location = $webresponse->headers['location']; + $parsed = parse_url($location); + $query = array(); + parse_str($parsed['query'], $query); + $query = Auth_OpenID::fixArgs($query); - $this->assertEquals(Auth_OpenID_REMOTE_ERROR, $status); - $resultArgs = Auth_OpenID_KVForm::toArray($info); - $this->assertTrue(array_key_exists('error', $resultArgs)); + $this->assertTrue(array_key_exists('openid.sig', $query)); + $this->assertTrue(array_key_exists('openid.assoc_handle', $query)); + $this->assertTrue(array_key_exists('openid.signed', $query)); } - function assertKeyExists($key, $ary) + function test_idresDumb() { - $this->assertTrue(array_key_exists($key, $ary), - "Failed to find $key in $ary"); + $webresponse = $this->encoder->encode($this->response); + $this->assertEquals($webresponse->code, AUTH_OPENID_HTTP_REDIRECT); + $this->assertTrue(array_key_exists('location', $webresponse->headers)); + + $location = $webresponse->headers['location']; + $parsed = parse_url($location); + $query = array(); + parse_str($parsed['query'], $query); + $query = Auth_OpenID::fixArgs($query); + $this->assertTrue(array_key_exists('openid.sig', $query)); + $this->assertTrue(array_key_exists('openid.assoc_handle', $query)); + $this->assertTrue(array_key_exists('openid.signed', $query)); } - function assertKeyAbsent($key, $ary) + function test_forgotStore() { - $this->assertFalse(array_key_exists($key, $ary), - "Unexpectedly found $key in $ary"); + $this->encoder->signatory = null; + $result = $this->encoder->encode($this->response); + if (!is_a($result, 'Auth_OpenID_ServerError')) { + $this->fail(sprintf("Expected Auth_OpenID_ServerError, got %s", + gettype($result))); + } } - function test_associatePlain() + function test_cancel() { - list($status, $info) = $this->server->associate(array()); + $request = new Auth_OpenID_CheckIDRequest( + 'http://bombom.unittest/', + 'http://burr.unittest/', + 'http://burr.unittest/999', + false); - $this->assertEquals(Auth_OpenID_REMOTE_OK, $status); - $ra = Auth_OpenID_KVForm::toArray($info); - $this->assertEquals('HMAC-SHA1', $ra['assoc_type']); - $this->assertKeyAbsent('session_type', $ra); - $this->assertKeyExists('assoc_handle', $ra); - $this->assertKeyExists('mac_key', $ra); - $exp = (integer)$ra['expires_in']; - $this->assertTrue($exp > 0); + $response = new Auth_OpenID_CheckIDResponse($request, 'cancel'); + $webresponse = $this->encoder->encode($response); + $this->assertEquals($webresponse->code, AUTH_OPENID_HTTP_REDIRECT); + $this->assertTrue(array_key_exists('location', $webresponse->headers)); + $location = $webresponse->headers['location']; + $parsed = parse_url($location); + $query = array(); + parse_str($parsed['query'], $query); + $query = Auth_OpenID::fixArgs($query); + $this->assertFalse(array_key_exists('openid.sig', $query)); + } + + function test_assocReply() + { + $request = new Auth_OpenID_AssociateRequest(); + $response = new Auth_OpenID_ServerResponse($request); + $response->fields = array('assoc_handle' => "every-zig"); + $webresponse = $this->encoder->encode($response); + $body = "assoc_handle:every-zig\n"; + + $this->assertEquals($webresponse->code, AUTH_OPENID_HTTP_OK); + $this->assertEquals($webresponse->headers, array()); + $this->assertEquals($webresponse->body, $body); + } + + function test_alreadySigned() + { + $this->response->fields['sig'] = 'priorSig=='; + $result = $this->encoder->encode($this->response); + if (!is_a($result, 'Auth_OpenID_AlreadySigned')) { + $this->fail(sprintf("Expected Auth_OpenID_AlreadySigned " . + "instance, got %s", gettype($result))); + } + } +} + +class Tests_Auth_OpenID_CheckID extends PHPUnit_TestCase { + function setUp() + { + $this->request = new Auth_OpenID_CheckIDRequest( + 'http://bambam.unittest/', + 'http://bar.unittest/999', + 'http://bar.unittest/', + false); } - function test_associateDHdefaults() + function test_trustRootInvalid() { - if (defined('Auth_OpenID_NO_MATH_SUPPORT')) { + $this->request->trust_root = "http://foo.unittest/17"; + $this->request->return_to = "http://foo.unittest/39"; + $this->assertFalse($this->request->trustRootValid()); + } + + function test_trustRootValid() + { + $this->request->trust_root = "http://foo.unittest/"; + $this->request->return_to = "http://foo.unittest/39"; + $this->assertTrue($this->request->trustRootValid()); + } + + function test_answerToInvalidRoot() + { + $this->request->trust_root = "http://foo.unittest/17"; + $this->request->return_to = "http://foo.unittest/39"; + $result = $this->request->answer(true); + if (!is_a($result, 'Auth_OpenID_UntrustedReturnURL')) { + $this->fail(sprintf("Expected Auth_OpenID_UntrustedReturnURL, " . + "got %s", gettype($result))); + } + $this->assertTrue($this->request->answer(false)); + } + + function test_answerAllow() + { + $answer = $this->request->answer(true); + + if (_isError($answer)) { + $this->fail($answer->toString()); return; } - $dh = new Auth_OpenID_DiffieHellman(); - $args = $dh->getAssocArgs(); - list($status, $info) = $this->server->associate($args); - $this->assertEquals(Auth_OpenID_REMOTE_OK, $status); - - $ra = Auth_OpenID_KVForm::toArray($info); - $this->assertEquals('HMAC-SHA1', $ra['assoc_type']); - $this->assertEquals('DH-SHA1', $ra['session_type']); - $this->assertKeyExists('assoc_handle', $ra); - $this->assertKeyExists('dh_server_public', $ra); - $this->assertKeyAbsent('mac_key', $ra); - $exp = (integer)$ra['expires_in']; - $this->assertTrue($exp > 0); - $secret = $dh->consumerFinish($ra); - $this->assertEquals('string', gettype($secret)); - $this->assertTrue(strlen($secret) > 0); - } - - function test_associateDHnoKey() - { - $args = array('openid.session_type' => 'DH-SHA1'); - list($status, $info) = $this->server->associate($args); - if (defined('Auth_OpenID_NO_MATH_SUPPORT')) { - $this->assertEquals(Auth_OpenID_REMOTE_OK, $status); - $ra = Auth_OpenID_KVForm::toArray($info); - $this->assertEquals('HMAC-SHA1', $ra['assoc_type']); - $this->assertKeyExists('assoc_handle', $ra); - $this->assertKeyExists('mac_key', $ra); - $exp = (integer)$ra['expires_in']; - $this->assertTrue($exp > 0); + $this->assertEquals($answer->request, $this->request); + $this->assertEquals($answer->fields, array( + 'mode' => 'id_res', + 'identity' => $this->request->identity, + 'return_to' => $this->request->return_to)); + + $this->assertEquals($answer->signed, + array("mode", "identity", "return_to")); + } + + function test_answerAllowNoTrustRoot() + { + $this->request->trust_root = null; + $answer = $this->request->answer(true); + $this->assertEquals($answer->request, $this->request); + $this->assertEquals($answer->fields, array( + 'mode' => 'id_res', + 'identity' => $this->request->identity, + 'return_to' => $this->request->return_to)); + + $this->assertEquals($answer->signed, + array("mode", "identity", "return_to")); + } + + function test_answerImmediateDeny() + { + $this->request->mode = 'checkid_immediate'; + $this->request->immediate = true; + $server_url = "http://setup-url.unittest/"; + $answer = $this->request->answer(false, $server_url); + $this->assertEquals($answer->request, $this->request); + $this->assertEquals(count($answer->fields), 2); + $this->assertEquals(Auth_OpenID::arrayGet($answer->fields, 'mode'), + 'id_res'); + $this->assertTrue(strpos(Auth_OpenID::arrayGet($answer->fields, + 'user_setup_url'), + $server_url) == 0); + + $this->assertEquals($answer->signed, array()); + } + + function test_answerSetupDeny() + { + $answer = $this->request->answer(false); + $this->assertEquals($answer->fields, array('mode' => 'cancel')); + $this->assertEquals($answer->signed, array()); + } + + function test_getCancelURL() + { + $url = $this->request->getCancelURL(); + $expected = $this->request->return_to . '?openid.mode=cancel'; + $this->assertEquals($url, $expected); + } + + function test_getCancelURLimmed() + { + $this->request->mode = 'checkid_immediate'; + $this->request->immediate = true; + $result = $this->request->getCancelURL(); + if (!is_a($result, 'Auth_OpenID_ServerError')) { + $this->fail(sprintf("Expected Auth_OpenID_ServerError, got %s", + gettype($result))); + } + } +} + +class Tests_Auth_OpenID_CheckIDExtension extends PHPUnit_TestCase { + + function setUp() + { + $this->request = new Auth_OpenID_CheckIDRequest( + 'http://bambam.unittest/', + 'http://bar.unittest/', + 'http://bar.unittest/999', + false); + + $this->response = new Auth_OpenID_CheckIDResponse($this->request); + $this->response->fields['blue'] = 'star'; + } + + function test_addField() + { + $namespace = 'mj12'; + $this->response->addField($namespace, 'bright', 'potato'); + $this->assertEquals($this->response->fields, + array('blue' => 'star', + 'mode' => 'id_res', + 'mj12.bright' => 'potato')); + $this->assertEquals($this->response->signed, + array('mode', 'identity', 'return_to', + 'mj12.bright')); + } + + function test_addFieldUnsigned() + { + $namespace = 'mj12'; + $this->response->addField($namespace, 'dull', 'lemon', false); + $this->assertEquals($this->response->fields, + array('blue' => 'star', + 'mode' => 'id_res', + 'mj12.dull' => 'lemon')); + $this->assertEquals($this->response->signed, + array('mode', 'identity', 'return_to')); + } + + function test_addFields() + { + $namespace = 'mi5'; + $this->response->addFields($namespace, array('tangy' => 'suspenders', + 'bravo' => 'inclusion')); + $this->assertEquals($this->response->fields, + array('blue' => 'star', + 'mode' => 'id_res', + 'mi5.tangy' => 'suspenders', + 'mi5.bravo' => 'inclusion')); + $this->assertEquals($this->response->signed, + array('mode', 'identity', 'return_to', + 'mi5.tangy', 'mi5.bravo')); + } + + function test_addFieldsUnsigned() + { + $namespace = 'mi5'; + $this->response->addFields($namespace, array('strange' => 'conditioner', + 'elemental' => 'blender'), + false); + $this->assertEquals($this->response->fields, + array('blue' => 'star', + 'mode' => 'id_res', + 'mi5.strange' => 'conditioner', + 'mi5.elemental' => 'blender')); + $this->assertEquals($this->response->signed, + array('mode', 'identity', 'return_to')); + } + + function test_update() + { + $eresponse = new Auth_OpenID_ServerResponse(null); + $eresponse->fields = array('shape' => 'heart', + 'content' => 'strings,wire'); + $eresponse->signed = array('content'); + $this->response->update('box', $eresponse); + $this->assertEquals($this->response->fields, + array('blue' => 'star', + 'mode' => 'id_res', + 'box.shape' => 'heart', + 'box.content' => 'strings,wire')); + $this->assertEquals($this->response->signed, + array('mode', 'identity', 'return_to', 'content')); + } +} + +class _MockSignatory { + var $isValid = true; + + function _MockSignatory($assoc) + { + $this->assocs = array($assoc); + } + + function verify($assoc_handle, $sig, $signed_pairs) + { + if (!$sig) { + return false; + } + + if (!is_array($signed_pairs)) { + return false; + } + + if (in_array(array(true, $assoc_handle), $this->assocs)) { + return $this->isValid; } else { - $this->assertEquals(Auth_OpenID_REMOTE_ERROR, $status); - $ra = Auth_OpenID_KVForm::toArray($info); - $this->assertKeyExists('error', $ra); + return false; } } - function _buildURL($base, $query) + function getAssociation($assoc_handle, $dumb) { - $result = $base; - $div = '?'; - foreach ($query as $k => $v) { - $result .= sprintf("%s%s=%s", $div, urlencode($k), urlencode($v)); - $div = '&'; + if (in_array(array($dumb, $assoc_handle), $this->assocs)) { + // This isn't a valid implementation for many uses of this + // function, mind you. + return true; + } else { + return null; } - return $result; } - function _startAuth($mode, $authorized) + function invalidate($assoc_handle, $dumb) { - $args = array( - 'openid.mode' => $mode, - 'openid.identity' => $this->id_url, - 'openid.return_to' => $this->rt_url, - ); - $ainfo = new Auth_OpenID_ServerRequest($this->sv_url, $args); - return $this->server->getAuthResponse(&$ainfo, $authorized); + if (in_array(array($dumb, $assoc_handle), $this->assocs)) { + $i = 0; + foreach ($this->assocs as $pair) { + if ($pair == array($dumb, $assoc_handle)) { + unset($this->assocs[$i]); + break; + } + $i++; + } + } } +} - function test_checkIdImmediateFailure() +class Tests_Auth_OpenID_CheckAuth extends PHPUnit_TestCase { + function setUp() { - $ret = $this->_startAuth('checkid_immediate', false); - list($base, $query) = $this->_parseRedirResp($ret); + $this->assoc_handle = 'mooooooooo'; + $this->request = new Auth_OpenID_CheckAuthRequest( + $this->assoc_handle, 'signarture', + array(array('one', 'alpha'), + array('two', 'beta'))); - $setup_args = array('openid.identity' => $this->id_url, - 'openid.mode' => 'checkid_setup', - 'openid.return_to' => $this->rt_url, - ); - $setup_url = $this->_buildURL($this->sv_url, $setup_args); + $this->signatory = new _MockSignatory(array(true, $this->assoc_handle)); + } - $eargs = array('openid.mode' => 'id_res', - 'openid.user_setup_url' => $setup_url); + function test_valid() + { + $r = $this->request->answer($this->signatory); + $this->assertEquals($r->fields, array('is_valid' => 'true')); + $this->assertEquals($r->request, $this->request); + } - $this->assertEquals($eargs, $query); - $this->assertEquals($this->rt_url, $base); + function test_invalid() + { + $this->signatory->isValid = false; + $r = $this->request->answer($this->signatory); + $this->assertEquals($r->fields, array('is_valid' => 'false')); } - function _checkIDGood($mode) + function test_replay() { - $ret = $this->_startAuth($mode, true); - list($base, $query) = $this->_parseRedirResp($ret); - $this->assertEquals($base, $this->rt_url); - $this->assertEquals($query['openid.mode'], 'id_res'); - $this->assertEquals($query['openid.identity'], $this->id_url); - $this->assertEquals($query['openid.return_to'], $this->rt_url); - $this->assertEquals('mode,identity,return_to', $query['openid.signed']); + $r = $this->request->answer($this->signatory); + $r = $this->request->answer($this->signatory); + $this->assertEquals($r->fields, array('is_valid' => 'false')); + } - $assoc = $this->store->getAssociation($this->server->_dumb_key, - $query['openid.assoc_handle']); - $this->assertNotNull($assoc); - $expected = $assoc->sign(array('mode' => 'id_res', - 'identity' => $this->id_url, - 'return_to' => $this->rt_url, - )); - $expected64 = base64_encode($expected); - $this->assertEquals($expected64, $query['openid.sig']); + function test_invalidatehandle() + { + $this->request->invalidate_handle = "bogusHandle"; + $r = $this->request->answer($this->signatory); + $this->assertEquals($r->fields, + array('is_valid' => 'true', + 'invalidate_handle' => "bogusHandle")); + $this->assertEquals($r->request, $this->request); } - function test_checkIdImmediate() + function test_invalidatehandleNo() { - $this->_checkIDGood('checkid_immediate'); + $assoc_handle = 'goodhandle'; + $this->signatory->assocs[] = array(false, 'goodhandle'); + $this->request->invalidate_handle = $assoc_handle; + $r = $this->request->answer($this->signatory); + $this->assertEquals($r->fields, array('is_valid' => 'true')); } +} + +class Tests_Auth_OpenID_Associate extends PHPUnit_TestCase { + // TODO: test DH with non-default values for modulus and gen. + // (important to do because we actually had it broken for a + // while.) - function test_checkIdSetup() + function setUp() { - $this->_checkIDGood('checkid_setup'); + $this->request = new Auth_OpenID_AssociateRequest(); + $this->store = new Tests_Auth_OpenID_MemStore(); + $this->signatory = new Auth_OpenID_Signatory($this->store); + $this->assoc = $this->signatory->createAssociation(false); } - function test_checkIdSetupNeedAuth() + function test_dh() { - $args = array( - 'openid.mode' => 'checkid_setup', - 'openid.identity' => $this->id_url, - 'openid.return_to' => $this->rt_url, - 'openid.trust_root' => $this->tr_url, - ); + $dh = new Auth_OpenID_DiffieHellman(); + $ml =& Auth_OpenID_getMathLib(); + + $this->request->session_type = 'DH-SHA1'; + $this->request->pubkey = $dh->public; + $response = $this->request->answer($this->assoc); + $this->assertEquals( + Auth_OpenID::arrayGet($response->fields, "assoc_type"), + "HMAC-SHA1"); + + $this->assertEquals( + Auth_OpenID::arrayGet($response->fields, "assoc_handle"), + $this->assoc->handle); + + $this->assertFalse( + Auth_OpenID::arrayGet($response->fields, "mac_key")); + + $this->assertEquals( + Auth_OpenID::arrayGet($response->fields, "session_type"), + "DH-SHA1"); + + $this->assertTrue( + Auth_OpenID::arrayGet($response->fields, "enc_mac_key")); + + $this->assertTrue( + Auth_OpenID::arrayGet($response->fields, + "dh_server_public")); + + $enc_key = base64_decode( + Auth_OpenID::arrayGet($response->fields, "enc_mac_key")); + + $spub = $ml->base64ToLong( + Auth_OpenID::arrayGet($response->fields, + "dh_server_public")); + + $secret = $dh->xorSecret($spub, $enc_key); + + $this->assertEquals($secret, $this->assoc->secret); + } + + function test_plaintext() + { + $response = $this->request->answer($this->assoc); + + $this->assertEquals( + Auth_OpenID::arrayGet($response->fields, "assoc_type"), + "HMAC-SHA1"); + + $this->assertEquals( + Auth_OpenID::arrayGet($response->fields, "assoc_handle"), + $this->assoc->handle); + + $this->assertEquals( + Auth_OpenID::arrayGet($response->fields, "expires_in"), + sprintf("%d", $this->signatory->SECRET_LIFETIME)); + + $this->assertEquals( + Auth_OpenID::arrayGet($response->fields, "mac_key"), + base64_encode($this->assoc->secret)); + + $this->assertFalse(Auth_OpenID::arrayGet($response->fields, + "session_type")); + + $this->assertFalse(Auth_OpenID::arrayGet($response->fields, + "enc_mac_key")); + + $this->assertFalse(Auth_OpenID::arrayGet($response->fields, + "dh_server_public")); + } +} + +class Counter { + function Counter() + { + $this->count = 0; + } + + function inc() + { + $this->count += 1; + } +} + +class Tests_Auth_OpenID_ServerTest extends PHPUnit_TestCase { + function setUp() + { + $this->store = new Tests_Auth_OpenID_MemStore(); + $this->server = new Auth_OpenID_Server($this->store); + } + + /* + * Leaving this test out because PHP doesn't really support this + * kind of runtime modification. + * + function test_dispatch() + { + $monkeycalled =& new Counter(); + function monkeyDo(request): + monkeycalled.inc(); + r = server.OpenIDResponse(request); + return r; + $this->server.openid_monkeymode = monkeyDo; + request = server.OpenIDRequest(); + request.mode = "monkeymode"; + webresult = $this->server.handleRequest(request); + $this->assertEquals(monkeycalled.count, 1); + */ + + function test_associate() + { + $request = new Auth_OpenID_AssociateRequest(); + $response = $this->server->openid_associate($request); + $this->assertTrue(array_key_exists('assoc_handle', + $response->fields)); + } + + function test_checkAuth() + { + $request = new Auth_OpenID_CheckAuthRequest('arrrrrf', + '0x3999', array()); - $ainfo = new Auth_OpenID_ServerRequest($this->sv_url, $args); - list($status, $info) = $this->server->getAuthResponse(&$ainfo, false); - $this->assertEquals(Auth_OpenID_DO_AUTH, $status); - $this->assertEquals($this->tr_url, $info->getTrustRoot()); - $this->assertEquals($this->id_url, $info->getIdentityURL()); + $response = $this->server->openid_check_authentication($request); + $this->assertTrue(array_key_exists('is_valid', + $response->fields)); } +} - function test_checkIdSetupCancel() +class Tests_Auth_OpenID_Signatory extends PHPUnit_TestCase { + function setUp() { - list($status, $info) = $this->_startAuth('checkid_setup', false); - $this->assertEquals(Auth_OpenID_DO_AUTH, $status); - list($base, $query) = $this->_parseRedirResp($info->cancel()); - $this->assertEquals($this->rt_url, $base); - $this->assertEquals('cancel', $query['openid.mode']); + $this->store = new Tests_Auth_OpenID_MemStore(); + $this->signatory = new Auth_OpenID_Signatory($this->store); + $this->dumb_key = $this->signatory->dumb_key; + $this->normal_key = $this->signatory->normal_key; } - function _setupCheckAuth() + function test_sign() { - $ret = $this->_startAuth('checkid_immediate', true); - list($base, $query) = $this->_parseRedirResp($ret); - $this->assertEquals($base, $this->rt_url); - $query['openid.mode'] = 'check_authentication'; - return $query; + $request = new Auth_OpenID_ServerRequest(); + $assoc_handle = '{assoc}{lookatme}'; + $this->store->storeAssociation( + $this->normal_key, + Auth_OpenID_Association::fromExpiresIn(60, $assoc_handle, + 'sekrit', 'HMAC-SHA1')); + $request->assoc_handle = $assoc_handle; + $response = new Auth_OpenID_CheckIDResponse($request); + $response->fields = array( + 'foo' => 'amsigned', + 'bar' => 'notsigned', + 'azu' => 'alsosigned'); + + $response->signed = array('foo', 'azu'); + $sresponse = $this->signatory->sign($response); + + $this->assertEquals(Auth_OpenID::arrayGet($sresponse->fields, + 'assoc_handle'), + $assoc_handle); + + $this->assertEquals(Auth_OpenID::arrayGet($sresponse->fields, 'signed'), + 'foo,azu'); + + $this->assertTrue(Auth_OpenID::arrayGet($sresponse->fields, 'sig')); + } + + function test_signDumb() + { + $request = new Auth_OpenID_ServerRequest(); + $request->assoc_handle = null; + $response = new Auth_OpenID_CheckIDResponse($request); + $response->fields = array( + 'foo' => 'amsigned', + 'bar' => 'notsigned', + 'azu' => 'alsosigned'); + + $response->signed = array('foo', 'azu'); + $sresponse = $this->signatory->sign($response); + + $assoc_handle = Auth_OpenID::arrayGet($sresponse->fields, + 'assoc_handle'); + + $this->assertTrue($assoc_handle); + $assoc = $this->signatory->getAssociation($assoc_handle, true); + + $this->assertTrue($assoc); + $this->assertEquals(Auth_OpenID::arrayGet($sresponse->fields, 'signed'), + 'foo,azu'); + $this->assertTrue(Auth_OpenID::arrayGet($sresponse->fields, 'sig')); } - function test_checkAuthentication() + function test_signExpired() { - $args = $this->_setupCheckAuth(); - list($status, $info) = $this->server->checkAuthentication($args); - $this->assertEquals(Auth_OpenID_REMOTE_OK, $status); - $this->assertEquals("is_valid:true\n", $info); + $request = new Auth_OpenID_ServerRequest(); + $assoc_handle = '{assoc}{lookatme}'; + $this->store->storeAssociation( + $this->normal_key, + Auth_OpenID_Association::fromExpiresIn(-10, $assoc_handle, + 'sekrit', 'HMAC-SHA1')); + $this->assertTrue($this->store->getAssociation($this->normal_key, + $assoc_handle)); + + $request->assoc_handle = $assoc_handle; + $response = new Auth_OpenID_CheckIDResponse($request); + $response->fields = array( + 'foo' => 'amsigned', + 'bar' => 'notsigned', + 'azu' => 'alsosigned'); + + $response->signed = array('foo', 'azu'); + $sresponse = $this->signatory->sign($response); + + $new_assoc_handle = Auth_OpenID::arrayGet($sresponse->fields, + 'assoc_handle'); + $this->assertTrue($new_assoc_handle); + $this->assertFalse($new_assoc_handle == $assoc_handle); + + $this->assertEquals(Auth_OpenID::arrayGet($sresponse->fields, + 'invalidate_handle'), + $assoc_handle); + + $this->assertEquals(Auth_OpenID::arrayGet($sresponse->fields, 'signed'), + 'foo,azu'); + $this->assertTrue(Auth_OpenID::arrayGet($sresponse->fields, 'sig')); + + // make sure the expired association is gone + $this->assertFalse($this->store->getAssociation($this->normal_key, + $assoc_handle)); + + // make sure the new key is a dumb mode association + $this->assertTrue($this->store->getAssociation($this->dumb_key, + $new_assoc_handle)); + + $this->assertFalse($this->store->getAssociation($this->normal_key, + $new_assoc_handle)); } - function test_checkAuthenticationFailSig() + function test_signInvalidHandle() { - $args = $this->_setupCheckAuth(); - $args['openid.sig'] = str_rot13($args['openid.sig']); - list($status, $info) = $this->server->checkAuthentication($args); - $this->assertEquals(Auth_OpenID_REMOTE_OK, $status); - $this->assertEquals("is_valid:false\n", $info); + $request = new Auth_OpenID_ServerRequest(); + $assoc_handle = '{bogus-assoc}{notvalid}'; + + $request->assoc_handle = $assoc_handle; + $response = new Auth_OpenID_CheckIDResponse($request); + $response->fields = array( + 'foo' => 'amsigned', + 'bar' => 'notsigned', + 'azu' => 'alsosigned'); + + $response->signed = array('foo', 'azu'); + $sresponse = $this->signatory->sign($response); + + $new_assoc_handle = Auth_OpenID::arrayGet($sresponse->fields, + 'assoc_handle'); + + $this->assertTrue($new_assoc_handle); + $this->assertFalse($new_assoc_handle == $assoc_handle); + + $this->assertEquals(Auth_OpenID::arrayGet($sresponse->fields, + 'invalidate_handle'), + $assoc_handle); + + $this->assertEquals(Auth_OpenID::arrayGet($sresponse->fields, 'signed'), + 'foo,azu'); + $this->assertTrue(Auth_OpenID::arrayGet($sresponse->fields, 'sig')); + + // make sure the new key is a dumb mode association + $this->assertTrue($this->store->getAssociation($this->dumb_key, + $new_assoc_handle)); + + $this->assertFalse($this->store->getAssociation($this->normal_key, + $new_assoc_handle)); } - function test_checkAuthenticationFailHandle() + function test_verify() { - $args = $this->_setupCheckAuth(); - $args['openid.assoc_handle'] = 'a bad handle'; - list($status, $info) = $this->server->checkAuthentication($args); - $this->assertEquals(Auth_OpenID_REMOTE_OK, $status); - $this->assertEquals("is_valid:false\n", $info); + $assoc_handle = '{vroom}{zoom}'; + $assoc = Auth_OpenID_Association::fromExpiresIn(60, $assoc_handle, + 'sekrit', 'HMAC-SHA1'); + + $this->store->storeAssociation($this->dumb_key, $assoc); + + $signed_pairs = array(array('foo', 'bar'), + array('apple', 'orange')); + + $sig = "Ylu0KcIR7PvNegB/K41KpnRgJl0="; + $verified = $this->signatory->verify($assoc_handle, $sig, + $signed_pairs); + $this->assertTrue($verified); + } + + function test_verifyBadSig() + { + $assoc_handle = '{vroom}{zoom}'; + $assoc = Auth_OpenID_Association::fromExpiresIn(60, $assoc_handle, + 'sekrit', 'HMAC-SHA1'); + + $this->store->storeAssociation($this->dumb_key, $assoc); + + $signed_pairs = array(array('foo', 'bar'), + array('apple', 'orange')); + + $sig = str_rot13("Ylu0KcIR7PvNegB/K41KpnRgJl0="); + $verified = $this->signatory->verify($assoc_handle, $sig, + $signed_pairs); + + $this->assertFalse($verified); + } + + function test_verifyBadHandle() + { + $assoc_handle = '{vroom}{zoom}'; + $signed_pairs = array(array('foo', 'bar'), + array('apple', 'orange')); + + $sig = "Ylu0KcIR7PvNegB/K41KpnRgJl0="; + $verified = $this->signatory->verify($assoc_handle, $sig, + $signed_pairs); + $this->assertFalse($verified); + } + + function test_getAssoc() + { + $assoc_handle = $this->makeAssoc(true); + $assoc = $this->signatory->getAssociation($assoc_handle, true); + $this->assertTrue($assoc); + $this->assertEquals($assoc->handle, $assoc_handle); + } + + function test_getAssocExpired() + { + $assoc_handle = $this->makeAssoc(true, -10); + $assoc = $this->signatory->getAssociation($assoc_handle, true); + $this->assertFalse($assoc); + } + + function test_getAssocInvalid() + { + $ah = 'no-such-handle'; + $this->assertEquals( + $this->signatory->getAssociation($ah, false), null); + } + + function test_getAssocDumbVsNormal() + { + $assoc_handle = $this->makeAssoc(true); + $this->assertEquals( + $this->signatory->getAssociation($assoc_handle, false), null); + } + + function test_createAssociation() + { + $assoc = $this->signatory->createAssociation(false); + $this->assertTrue($this->signatory->getAssociation($assoc->handle, + false)); + } + + function makeAssoc($dumb, $lifetime = 60) + { + $assoc_handle = '{bling}'; + $assoc = Auth_OpenID_Association::fromExpiresIn( + $lifetime, $assoc_handle, + 'sekrit', 'HMAC-SHA1'); + + $this->store->storeAssociation((($dumb) ? $this->dumb_key : + $this->normal_key), $assoc); + return $assoc_handle; + } + + function test_invalidate() + { + $assoc_handle = '-squash-'; + $assoc = Auth_OpenID_Association::fromExpiresIn(60, $assoc_handle, + 'sekrit', 'HMAC-SHA1'); + + $this->store->storeAssociation($this->dumb_key, $assoc); + $assoc = $this->signatory->getAssociation($assoc_handle, true); + $this->assertTrue($assoc); + $assoc = $this->signatory->getAssociation($assoc_handle, true); + $this->assertTrue($assoc); + $this->signatory->invalidate($assoc_handle, true); + $assoc = $this->signatory->getAssociation($assoc_handle, true); + $this->assertFalse($assoc); + } +} + +class Tests_Auth_OpenID_Server extends PHPUnit_TestSuite { + + function getName() + { + return "Tests_Auth_OpenID_Server"; + } + + function Tests_Auth_OpenID_Server() + { + $this->addTestSuite('Tests_Auth_OpenID_Signatory'); + $this->addTestSuite('Tests_Auth_OpenID_ServerTest'); + $this->addTestSuite('Tests_Auth_OpenID_Associate'); + $this->addTestSuite('Tests_Auth_OpenID_CheckAuth'); + $this->addTestSuite('Tests_Auth_OpenID_CheckIDExtension'); + $this->addTestSuite('Tests_Auth_OpenID_CheckAuth'); + $this->addTestSuite('Tests_Auth_OpenID_SigningEncode'); + $this->addTestSuite('Tests_Auth_OpenID_Test_Encode'); + $this->addTestSuite('Tests_Auth_OpenID_Test_Decode'); + $this->addTestSuite('Tests_Auth_OpenID_Test_ServerError'); + $this->addTestSuite('Tests_Auth_OpenID_CheckID'); } } + +?>
\ No newline at end of file |