diff options
author | tailor <cygnus@janrain.com> | 2007-01-16 17:47:43 +0000 |
---|---|---|
committer | tailor <cygnus@janrain.com> | 2007-01-16 17:47:43 +0000 |
commit | 9433592429616e83afac58f1309652201c97e05c (patch) | |
tree | 0a98b7f56802d072ca09fa8a707c2c5f87c64c51 /Auth | |
parent | 42288029f3758515c81249416724861919e10621 (diff) | |
download | php-openid-9433592429616e83afac58f1309652201c97e05c.zip php-openid-9433592429616e83afac58f1309652201c97e05c.tar.gz php-openid-9433592429616e83afac58f1309652201c97e05c.tar.bz2 |
[project @ Refactored consumer from python implementation]
Diffstat (limited to 'Auth')
-rw-r--r-- | Auth/OpenID.php | 13 | ||||
-rw-r--r-- | Auth/OpenID/Consumer.php | 493 | ||||
-rw-r--r-- | Auth/OpenID/Discover.php | 31 |
3 files changed, 403 insertions, 134 deletions
diff --git a/Auth/OpenID.php b/Auth/OpenID.php index de65eac..d5870d3 100644 --- a/Auth/OpenID.php +++ b/Auth/OpenID.php @@ -166,6 +166,15 @@ class Auth_OpenID { } } + function addPrefix($values, $prefix) + { + $new_values = array(); + foreach ($values as $s) { + $new_values[] = $prefix . $s; + } + return $new_values; + } + /** * Convenience function for getting array values. * @@ -193,6 +202,10 @@ class Auth_OpenID { */ function parse_str($query) { + if ($query === null) { + return null; + } + $parts = explode('&', $query); $new_parts = array(); diff --git a/Auth/OpenID/Consumer.php b/Auth/OpenID/Consumer.php index d7c860e..061e437 100644 --- a/Auth/OpenID/Consumer.php +++ b/Auth/OpenID/Consumer.php @@ -287,15 +287,9 @@ class Auth_OpenID_Consumer { */ function begin($user_url) { - $discoverMethod = '_Auth_OpenID_discoverServiceList'; + $discoverMethod = 'Auth_OpenID_discover'; $openid_url = $user_url; - if (Services_Yadis_identifierScheme($user_url) == 'XRI') { - $discoverMethod = '_Auth_OpenID_discoverXRIServiceList'; - } else { - $openid_url = Auth_OpenID::normalizeUrl($user_url); - } - $disco =& new Services_Yadis_Discovery($this->session, $openid_url, $this->session_key_prefix); @@ -544,10 +538,8 @@ class Auth_OpenID_GenericConsumer { return $r; } - function complete($message, $endpoint) + function complete($message, $endpoint, $return_to = null) { - global $Auth_OpenID_OPENID1_NS; - $mode = $message->getArg(Auth_OpenID_OPENID_NS, 'mode', '<no mode set>'); @@ -556,23 +548,15 @@ class Auth_OpenID_GenericConsumer { } else if ($mode == 'error') { $error = $message->getArg(Auth_OpenID_OPENID_NS, 'error'); return new Auth_OpenID_FailureResponse($endpoint, $error); - } else if ($mode == 'id_res') { - if ($endpoint->claimed_id === null) { - return new Auth_OpenID_FailureResponse($endpoint, - "No session state found"); - } - - $response = $this->_doIdRes($message, $endpoint); + } else if ($message->isOpenID2() && ($mode == 'setup_needed')) { + return new Auth_OpenID_SetupNeededResponse($endpoint); - if ($response === null) { - return new Auth_OpenID_FailureResponse($endpoint, - "HTTP request failed"); - } - if ($response->status == Auth_OpenID_SUCCESS) { - return $this->_checkNonce($endpoint->server_url, - $response); + } else if ($mode == 'id_res') { + if ($this->_checkSetupNeeded($message)) { + return SetupNeededResponse($endpoint, + $result->user_setup_url); } else { - return $response; + return $this->_doIdRes($message, $endpoint); } } else { return new Auth_OpenID_FailureResponse($endpoint, @@ -581,92 +565,404 @@ class Auth_OpenID_GenericConsumer { } } + function _checkSetupNeeded($message) + { + // In OpenID 1, we check to see if this is a cancel from + // immediate mode by the presence of the user_setup_url + // parameter. + if ($message->isOpenID1()) { + $user_setup_url = $message->getArg(Auth_OpenID_OPENID1_NS, + 'user_setup_url'); + if ($user_setup_url !== null) { + return true; + } + } + + return false; + } + /** * @access private */ function _doIdRes($message, $endpoint) { - $user_setup_url = $message->getArg(Auth_OpenID_OPENID_NS, - 'user_setup_url'); + $signed_list_str = $message->getArg(Auth_OpenID_OPENID_NS, + 'signed'); + + if ($signed_list_str === null) { + // raise ProtocolError("Response missing signed list") + return new Auth_OpenID_FailureResponse($endpoint, + "Response missing signed list"); + } + + $signed_list = explode(',', $signed_list_str); + + // Checks for presence of appropriate fields (and checks + // signed list fields) + $result = $this->_idResCheckForFields($message, $signed_list); + + if (is_a($result, 'Auth_OpenID_FailureResponse')) { + return $result; + } + + // Verify discovery information: + $result = $this->_verifyDiscoveryResults($message, $endpoint); + + if (is_a($result, 'Auth_OpenID_FailureResponse')) { + return $result; + } + + $endpoint = $result; + + $result = $this->_idResCheckSignature($message, + $endpoint->server_url); + + if (is_a($result, 'Auth_OpenID_FailureResponse')) { + return $result; + } + + $response_identity = $message->getArg(Auth_OpenID_OPENID_NS, + 'identity'); + + $result = $this->_idResCheckNonce($message, $endpoint); + + if (is_a($result, 'Auth_OpenID_FailureResponse')) { + return $result; + } + + $signed_fields = Auth_OpenID::addPrefix($signed_list, "openid."); + + return new Auth_OpenID_SuccessResponse($endpoint, $message, + $signed_fields); + + } + + function _idResCheckSignature($message, $server_url) + { + $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS, + 'assoc_handle'); + + $assoc = $this->store->getAssociation($server_url, $assoc_handle); + + if ($assoc) { + if ($assoc->getExpiresIn() <= 0) { + // XXX: It might be a good idea sometimes to re-start + // the authentication with a new association. Doing it + // automatically opens the possibility for + // denial-of-service by a server that just returns + // expired associations (or really short-lived + // associations) + return new Auth_OpenID_FailureResponse(null, + 'Association with ' . $server_url . ' expired'); + } - if ($user_setup_url !== null) { - return new Auth_OpenID_SetupNeededResponse($endpoint, - $user_setup_url); + if (!$assoc->checkMessageSignature($message)) { + return new Auth_OpenID_FailureResponse(null, + "Bad signature"); + } + } else { + // It's not an association we know about. Stateless mode + // is our only possible path for recovery. XXX - async + // framework will not want to block on this call to + // _checkAuth. + if (!$this->_checkAuth($message, $server_url)) { + return new Auth_OpenID_FailureResponse(null, + "Server denied check_authentication"); + } } - $return_to = $message->getArg(Auth_OpenID_OPENID_NS, 'return_to'); - $server_id2 = $message->getArg(Auth_OpenID_OPENID_NS, 'identity'); - $assoc_handle = $message->getArg(Auth_OpenID_OPENID_NS, 'assoc_handle'); + return null; + } - if (($return_to === null) || - ($server_id2 === null) || - ($assoc_handle === null)) { + function _verifyDiscoveryResults($message, $endpoint) + { + if ($message->getOpenIDNamespace() == Auth_OpenID_OPENID2_NS) { + return $this->_verifyDiscoveryResultsOpenID2($message, + $endpoint); + } else { + return $this->_verifyDiscoveryResultsOpenID1($message, + $endpoint); + } + } + + function _verifyDiscoveryResultsOpenID1($message, $endpoint) + { + if ($endpoint === null) { return new Auth_OpenID_FailureResponse($endpoint, - "Missing required field"); + 'When using OpenID 1, the claimed ID must be supplied, ' . + 'either by passing it through as a return_to parameter ' . + 'or by using a session, and supplied to the GenericConsumer ' . + 'as the argument to complete()'); } - if ($endpoint->getLocalID() != $server_id2) { + $to_match = new Auth_OpenID_ServiceEndpoint(); + $to_match->type_uris = array(Auth_OpenID_TYPE_1_1); + $to_match->local_id = $message->getArg(Auth_OpenID_OPENID1_NS, + 'identity'); + + // Restore delegate information from the initiation phase + $to_match->claimed_id = $endpoint->claimed_id; + + if ($to_match->local_id === null) { + // raise ProtocolError('Missing required field openid.identity') return new Auth_OpenID_FailureResponse($endpoint, - "Server ID (delegate) mismatch"); + "Missing required field openid.identity"); } - $signed = $message->getArg(Auth_OpenID_OPENID_NS, 'signed'); - if ($signed) { - $signed_list = explode(",", $signed); + $result = $this->_verifyDiscoverySingle($endpoint, $to_match); + + if (is_a($result, 'Auth_OpenID_FailureResponse')) { + return $result; } else { - $signed_list = array(); + return $endpoint; } + } - $new_signed_list = array(); - foreach ($signed_list as $f) { - $new_signed_list[] = 'openid.'.$f; + function _verifyDiscoverySingle($endpoint, $to_match) + { + // Every type URI that's in the to_match endpoint has to be + // present in the discovered endpoint. + foreach ($to_match->type_uris as $type_uri) { + if (!$endpoint->usesExtension($type_uri)) { + // raise ProtocolError( + // 'Required type %r not present' % (type_uri,)) + return new Auth_OpenID_FailureResponse($endpoint, + "Required type ".$type_uri." not present"); + } } - $assoc = $this->store->getAssociation($endpoint->server_url, - $assoc_handle); + if ($to_match->claimed_id != $endpoint->claimed_id) { + return new Auth_OpenID_FailureResponse($endpoint, + sprintf('Claimed ID does not match (different subjects!), ' . + 'Expected %s, got %s', $to_match->claimed_id, + $endpoint->claimed_id)); + } - if ($assoc === null) { - // It's not an association we know about. Dumb mode is - // our only possible path for recovery. - if ($this->_checkAuth($message, $endpoint->server_url)) { - return new Auth_OpenID_SuccessResponse($endpoint, $message, - $new_signed_list); - } else { + if ($to_match->getLocalID() != $endpoint->getLocalID()) { + return new Auth_OpenID_FailureResponse($endpoint, + sprintf('local_id mismatch. Expected %s, got %s', + $to_match->getLocalID(), $endpoint->getLocalID())); + } + + // If the server URL is None, this must be an OpenID 1 + // response, because op_endpoint is a required parameter in + // OpenID 2. In that case, we don't actually care what the + // discovered server_url is, because signature checking or + // check_auth should take care of that check for us. + if ($to_match->server_url === null) { + if ($to_match->preferredNamespace() != Auth_OpenID_OPENID1_NS) { return new Auth_OpenID_FailureResponse($endpoint, - "Server denied check_authentication"); + "Preferred namespace mismatch (bug)"); } + } else if ($to_match->server_url != $endpoint->server_url) { + return new Auth_OpenID_FailureResponse($endpoint, + sprintf('OP Endpoint mismatch. Expected %s, got %s', + $to_match->server_url, $endpoint->server_url)); } - if ($assoc->getExpiresIn() <= 0) { - $msg = sprintf("Association with %s expired", - $endpoint->server_url); - return new Auth_OpenID_FailureResponse($endpoint, $msg); + return null; + } + + function _verifyDiscoveryResultsOpenID2($message, $endpoint) + { + $to_match = new Auth_OpenID_ServiceEndpoint(); + $to_match->type_uris = array(Auth_OpenID_TYPE_2_0); + $to_match->claimed_id = $message->getArg(Auth_OpenID_OPENID2_NS, + 'claimed_id'); + + $to_match->local_id = $message->getArg(Auth_OpenID_OPENID2_NS, + 'identity'); + + $to_match->server_url = $message->getArg(Auth_OpenID_OPENID2_NS, + 'op_endpoint'); + + if ($to_match->server_url === null) { + return new Auth_OpenID_FailureResponse($endpoint, + "OP Endpoint URL missing"); } - // Check the signature - $sig = $message->getArg(Auth_OpenID_OPENID_NS, 'sig'); - if (($sig === null) || - ($signed === null)) { + // claimed_id and identifier must both be present or both be + // absent + if (($to_match->claimed_id === null) && + ($to_match->local_id !== null)) { return new Auth_OpenID_FailureResponse($endpoint, - "Missing argument signature"); + 'openid.identity is present without openid.claimed_id'); + } else if (($to_match->claimed_id !== null) && + ($to_match->local_id === null)) { + return new Auth_OpenID_FailureResponse($endpoint, + 'openid.claimed_id is present without openid.identity'); + } else if ($to_match->claimed_id === null) { + // This is a response without identifiers, so there's + // really no checking that we can do, so return an + // endpoint that's for the specified `openid.op_endpoint' + return Auth_OpenID_ServiceEndpoint::fromOPEndpointURL( + $to_match->server_url); + } else if (!$endpoint) { + // The claimed ID doesn't match, so we have to do + // discovery again. This covers not using sessions, OP + // identifier endpoints and responses that didn't match + // the original request. + // oidutil.log('No pre-discovered information supplied.') + return $this->_discoverAndVerify($to_match); + } else if ($to_match->claimed_id != $endpoint->claimed_id) { + // oidutil.log('Mismatched pre-discovered session data. ' + // 'Claimed ID in session=%s, in assertion=%s' % + // (endpoint.claimed_id, to_match.claimed_id)) + return $this->_discoverAndVerify($to_match); + } else { + // The claimed ID matches, so we use the endpoint that we + // discovered in initiation. This should be the most + // common case. + $result = $this->_verifyDiscoverySingle($endpoint, $to_match); + + if (is_a($result, 'Auth_OpenID_FailureResponse')) { + return $result; + } + + return $endpoint; + } + + // Never reached. + } + + function _discoverAndVerify($to_match) + { + // oidutil.log('Performing discovery on %s' % (to_match.claimed_id,)) + list($unused, $services) = Auth_OpenID_discover($to_match->claimed_id); + if (!$services) { + // raise DiscoveryFailure('No OpenID information found at %s' % + // (to_match.claimed_id,), None) + return new Auth_OpenID_FailureResponse(null, + sprintf("No OpenID information found at %s", + $to_match->claimed_id)); + } + + // Search the services resulting from discovery to find one + // that matches the information from the assertion + $failure_messages = array(); + + foreach ($services as $endpoint) { + $result = $this->_verifyDiscoverySingle($endpoint, $to_match); + + if (is_a($result, 'Auth_OpenID_FailureResponse')) { + $failure_messages->append($result); + } else { + // It matches, so discover verification has + // succeeded. Return this endpoint. + return $endpoint; + } + } + + return new Auth_OpenID_FailureResponse(null, + sprintf('No matching endpoint found after discovering %s', + $to_match->claimed_id)); + } + + function _idResGetNonceOpenID1($message, $endpoint) + { + $return_to = $message->getArg(Auth_OpenID_OPENID1_NS, + 'return_to'); + if ($return_to === null) { + return null; } - //Fail if the identity field is present but not signed - if (($endpoint->claimed_id !== null) && - (!in_array('identity', $signed_list))) { - $msg = '"openid.identity" not signed'; - return new Auth_OpenID_FailureResponse($endpoint, $msg); + $parsed_url = parse_url($return_to); + + if (!array_key_exists('query', $parsed_url)) { + return null; } - $v_sig = $assoc->getMessageSignature($message); + $query = $parsed_url['query']; + $pairs = Auth_OpenID::parse_str($query); - if ($v_sig != $sig) { + if ($pairs === null) { + return null; + } + + foreach ($pairs as $k => $v) { + if ($k == Auth_OpenID_NONCE_NAME) { + return $v; + } + } + + return null; + } + + function _idResCheckNonce($message, $endpoint) + { + if ($message->isOpenID1()) { + // This indicates that the nonce was generated by the consumer + $nonce = $this->_idResGetNonceOpenID1($message, $endpoint); + $server_url = ''; + } else { + $nonce = $message->getArg(Auth_OpenID_OPENID2_NS, + 'response_nonce'); + + $server_url = $endpoint->server_url; + } + + if ($nonce === null) { return new Auth_OpenID_FailureResponse($endpoint, - "Bad signature"); + "Nonce missing from response"); } - return new Auth_OpenID_SuccessResponse($endpoint, - $message, $new_signed_list); + $parts = Auth_OpenID_splitNonce($nonce); + + if ($parts === null) { + return new Auth_OpenID_FailureResponse($endpoint, + "Malformed nonce in response"); + } + + list($timestamp, $salt) = $parts; + + if (!$this->store->useNonce($server_url, $timestamp, $salt)) { + return new Auth_OpenID_FailureResponse($endpoint, + "Nonce already used or out of range"); + } + + return null; + } + + function _idResCheckForFields($message, $signed_list) + { + $basic_fields = array('return_to', 'assoc_handle', 'sig'); + $basic_sig_fields = array('return_to', 'identity'); + + $require_fields = array( + Auth_OpenID_OPENID2_NS => array_merge($basic_fields, + array('op_endpoint')), + + Auth_OpenID_OPENID1_NS => array_merge($basic_fields, + array('identity')) + ); + + $require_sigs = array( + Auth_OpenID_OPENID2_NS => array_merge($basic_sig_fields, + array('response_nonce', + 'claimed_id', + 'assoc_handle')), + Auth_OpenID_OPENID1_NS => array_merge($basic_sig_fields, + array('nonce')) + ); + + foreach ($require_fields[$message->getOpenIDNamespace()] as $field) { + if (!$message->hasKey(Auth_OpenID_OPENID_NS, $field)) { + return new Auth_OpenID_FailureResponse(null, + "Missing required field '".$field."'"); + } + } + + foreach ($require_sigs[$message->getOpenIDNamespace()] as $field) { + // Field is present and not in signed list + if ($message->hasKey(Auth_OpenID_OPENID_NS, $field) && + (!in_array($field, $signed_list))) { + // raise ProtocolError('"%s" not signed' % (field,)) + return new Auth_OpenID_FailureResponse(null, + "'".$field."' not signed"); + } + } + + return null; } /** @@ -764,53 +1060,6 @@ class Auth_OpenID_GenericConsumer { /** * @access private */ - function _checkNonce($server_url, $response) - { - $nonce = $response->getNonce(); - if ($nonce === null) { - $parsed_url = parse_url($response->getReturnTo()); - $query_str = @$parsed_url['query']; - $query = array(); - parse_str($query_str, $query); - - $found = false; - - foreach ($query as $k => $v) { - if ($k == Auth_OpenID_NONCE_NAME) { - $server_url = ''; - $nonce = $v; - $found = true; - break; - } - } - - - if (!$found) { - return new Auth_OpenID_FailureResponse($response, - sprintf("Nonce missing from return_to: %s", - $response->getReturnTo())); - } - } - - list($timestamp, $salt) = Auth_OpenID_splitNonce($nonce); - - if (!($timestamp && $salt)) { - return new Auth_OpenID_FailureResponse($response, - 'Malformed nonce'); - } - - if (!$this->store->useNonce($server_url, - $timestamp, $salt)) { - return new Auth_OpenID_FailureResponse($response, - "Nonce missing from store"); - } - - return $response; - } - - /** - * @access private - */ function _getAssociation($endpoint) { if (!$this->_use_assocs) { diff --git a/Auth/OpenID/Discover.php b/Auth/OpenID/Discover.php index b244059..cc17f1b 100644 --- a/Auth/OpenID/Discover.php +++ b/Auth/OpenID/Discover.php @@ -359,19 +359,22 @@ function Auth_OpenID_discoverWithYadis($uri, &$fetcher) return array($yadis_url, $openid_services); } -function _Auth_OpenID_discoverServiceList($uri, &$fetcher) +function Auth_OpenID_discoverURI($uri, &$fetcher) { - list($url, $services) = Auth_OpenID_discoverWithYadis($uri, - $fetcher); + $parsed = parse_url($uri); - return $services; -} + if ($parsed && $parsed['scheme'] && $parsed['host']) { + if (!in_array($parsed['scheme'], array('http', 'https'))) { + // raise DiscoveryFailure('URI scheme is not HTTP or HTTPS', None) + return array($uri, array()); + } + } else { + $uri = 'http://' . $uri; + } -function _Auth_OpenID_discoverXRIServiceList($uri, &$fetcher) -{ - list($url, $services) = _Auth_OpenID_discoverXRI($uri, - $fetcher); - return $services; + $uri = Auth_OpenID::normalizeUrl($uri); + return Auth_OpenID_discoverWithYadis($uri, + $fetcher); } function Auth_OpenID_discoverWithoutYadis($uri, &$fetcher) @@ -393,7 +396,7 @@ function Auth_OpenID_discoverWithoutYadis($uri, &$fetcher) return array($identity_url, $openid_services); } -function _Auth_OpenID_discoverXRI($iname, &$fetcher) +function Auth_OpenID_discoverXRI($iname, &$fetcher) { $resolver = new Services_Yadis_ProxyResolver($fetcher); list($canonicalID, $yadis_services) = @@ -417,7 +420,11 @@ function _Auth_OpenID_discoverXRI($iname, &$fetcher) function Auth_OpenID_discover($uri, &$fetcher) { - return Auth_OpenID_discoverWithYadis($uri, $fetcher); + if (Services_Yadis_identifierScheme($uri) == 'XRI') { + return Auth_OpenID_discoverXRI($uri, $fetcher); + } else { + return Auth_OpenID_discoverURI($uri, $fetcher); + } } ?>
\ No newline at end of file |