<?php /** * Zend Framework * * LICENSE * * This source file is subject to the new BSD license that is bundled * with this package in the file LICENSE.txt. * It is also available through the world-wide-web at this URL: * http://framework.zend.com/license/new-bsd * If you did not receive a copy of the license and are unable to * obtain it through the world-wide-web, please send an email * to [email protected] so we can send you a copy immediately. * * @category Zend * @package Zend_OpenId * @subpackage Zend_OpenId_Provider * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License * @version $Id$ */ /** * @see Zend_OpenId */ require_once "Zend/OpenId.php"; /** * @see Zend_OpenId_Extension */ require_once "Zend/OpenId/Extension.php"; /** * OpenID provider (server) implementation * * @category Zend * @package Zend_OpenId * @subpackage Zend_OpenId_Provider * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ class Zend_OpenId_Provider { /** * Reference to an implementation of storage object * * @var Zend_OpenId_Provider_Storage $_storage */ private $_storage; /** * Reference to an implementation of user object * * @var Zend_OpenId_Provider_User $_user */ private $_user; /** * Time to live of association session in secconds * * @var integer $_sessionTtl */ private $_sessionTtl; /** * URL to peform interactive user login * * @var string $_loginUrl */ private $_loginUrl; /** * URL to peform interactive validation of consumer by user * * @var string $_trustUrl */ private $_trustUrl; /** * The OP Endpoint URL * * @var string $_opEndpoint */ private $_opEndpoint; /** * Constructs a Zend_OpenId_Provider object with given parameters. * * @param string $loginUrl is an URL that provides login screen for * end-user (by default it is the same URL with additional GET variable * openid.action=login) * @param string $trustUrl is an URL that shows a question if end-user * trust to given consumer (by default it is the same URL with additional * GET variable openid.action=trust) * @param Zend_OpenId_Provider_User $user is an object for communication * with User-Agent and store information about logged-in user (it is a * Zend_OpenId_Provider_User_Session object by default) * @param Zend_OpenId_Provider_Storage $storage is an object for keeping * persistent database (it is a Zend_OpenId_Provider_Storage_File object * by default) * @param integer $sessionTtl is a default time to live for association * session in seconds (1 hour by default). Consumer must reestablish * association after that time. */ public function __construct($loginUrl = null, $trustUrl = null, Zend_OpenId_Provider_User $user = null, Zend_OpenId_Provider_Storage $storage = null, $sessionTtl = 3600) { if ($loginUrl === null) { $loginUrl = Zend_OpenId::selfUrl() . '?openid.action=login'; } else { $loginUrl = Zend_OpenId::absoluteUrl($loginUrl); } $this->_loginUrl = $loginUrl; if ($trustUrl === null) { $trustUrl = Zend_OpenId::selfUrl() . '?openid.action=trust'; } else { $trustUrl = Zend_OpenId::absoluteUrl($trustUrl); } $this->_trustUrl = $trustUrl; if ($user === null) { require_once "Zend/OpenId/Provider/User/Session.php"; $this->_user = new Zend_OpenId_Provider_User_Session(); } else { $this->_user = $user; } if ($storage === null) { require_once "Zend/OpenId/Provider/Storage/File.php"; $this->_storage = new Zend_OpenId_Provider_Storage_File(); } else { $this->_storage = $storage; } $this->_sessionTtl = $sessionTtl; } /** * Sets the OP Endpoint URL * * @param string $url the OP Endpoint URL * @return null */ public function setOpEndpoint($url) { $this->_opEndpoint = $url; } /** * Registers a new user with given $id and $password * Returns true in case of success and false if user with given $id already * exists * * @param string $id user identity URL * @param string $password encoded user password * @return bool */ public function register($id, $password) { if (!Zend_OpenId::normalize($id) || empty($id)) { return false; } return $this->_storage->addUser($id, md5($id.$password)); } /** * Returns true if user with given $id exists and false otherwise * * @param string $id user identity URL * @return bool */ public function hasUser($id) { if (!Zend_OpenId::normalize($id)) { return false; } return $this->_storage->hasUser($id); } /** * Performs login of user with given $id and $password * Returns true in case of success and false otherwise * * @param string $id user identity URL * @param string $password user password * @return bool */ public function login($id, $password) { if (!Zend_OpenId::normalize($id)) { return false; } if (!$this->_storage->checkUser($id, md5($id.$password))) { return false; } $this->_user->setLoggedInUser($id); return true; } /** * Performs logout. Clears information about logged in user. * * @return void */ public function logout() { $this->_user->delLoggedInUser(); return true; } /** * Returns identity URL of current logged in user or false * * @return mixed */ public function getLoggedInUser() { return $this->_user->getLoggedInUser(); } /** * Retrieve consumer's root URL from request query. * Returns URL or false in case of failure * * @param array $params query arguments * @return mixed */ public function getSiteRoot($params) { $version = 1.1; if (isset($params['openid_ns']) && $params['openid_ns'] == Zend_OpenId::NS_2_0) { $version = 2.0; } if ($version >= 2.0 && isset($params['openid_realm'])) { $root = $params['openid_realm']; } else if ($version < 2.0 && isset($params['openid_trust_root'])) { $root = $params['openid_trust_root']; } else if (isset($params['openid_return_to'])) { $root = $params['openid_return_to']; } else { return false; } if (Zend_OpenId::normalizeUrl($root) && !empty($root)) { return $root; } return false; } /** * Allows consumer with given root URL to authenticate current logged * in user. Returns true on success and false on error. * * @param string $root root URL * @param mixed $extensions extension object or array of extensions objects * @return bool */ public function allowSite($root, $extensions=null) { $id = $this->getLoggedInUser(); if ($id === false) { return false; } if ($extensions !== null) { $data = array(); Zend_OpenId_Extension::forAll($extensions, 'getTrustData', $data); } else { $data = true; } $this->_storage->addSite($id, $root, $data); return true; } /** * Prohibit consumer with given root URL to authenticate current logged * in user. Returns true on success and false on error. * * @param string $root root URL * @return bool */ public function denySite($root) { $id = $this->getLoggedInUser(); if ($id === false) { return false; } $this->_storage->addSite($id, $root, false); return true; } /** * Delete consumer with given root URL from known sites of current logged * in user. Next time this consumer will try to authenticate the user, * Provider will ask user's confirmation. * Returns true on success and false on error. * * @param string $root root URL * @return bool */ public function delSite($root) { $id = $this->getLoggedInUser(); if ($id === false) { return false; } $this->_storage->addSite($id, $root, null); return true; } /** * Returns list of known consumers for current logged in user or false * if he is not logged in. * * @return mixed */ public function getTrustedSites() { $id = $this->getLoggedInUser(); if ($id === false) { return false; } return $this->_storage->getTrustedSites($id); } /** * Handles HTTP request from consumer * * @param array $params GET or POST variables. If this parameter is omited * or set to null, then $_GET or $_POST superglobal variable is used * according to REQUEST_METHOD. * @param mixed $extensions extension object or array of extensions objects * @param Zend_Controller_Response_Abstract $response an optional response * object to perform HTTP or HTML form redirection * @return mixed */ public function handle($params=null, $extensions=null, Zend_Controller_Response_Abstract $response = null) { if ($params === null) { if ($_SERVER["REQUEST_METHOD"] == "GET") { $params = $_GET; } else if ($_SERVER["REQUEST_METHOD"] == "POST") { $params = $_POST; } else { return false; } } $version = 1.1; if (isset($params['openid_ns']) && $params['openid_ns'] == Zend_OpenId::NS_2_0) { $version = 2.0; } if (isset($params['openid_mode'])) { if ($params['openid_mode'] == 'associate') { $response = $this->_associate($version, $params); $ret = ''; foreach ($response as $key => $val) { $ret .= $key . ':' . $val . "\n"; } return $ret; } else if ($params['openid_mode'] == 'checkid_immediate') { $ret = $this->_checkId($version, $params, 1, $extensions, $response); if (is_bool($ret)) return $ret; if (!empty($params['openid_return_to'])) { Zend_OpenId::redirect($params['openid_return_to'], $ret, $response); } return true; } else if ($params['openid_mode'] == 'checkid_setup') { $ret = $this->_checkId($version, $params, 0, $extensions, $response); if (is_bool($ret)) return $ret; if (!empty($params['openid_return_to'])) { Zend_OpenId::redirect($params['openid_return_to'], $ret, $response); } return true; } else if ($params['openid_mode'] == 'check_authentication') { $response = $this->_checkAuthentication($version, $params); $ret = ''; foreach ($response as $key => $val) { $ret .= $key . ':' . $val . "\n"; } return $ret; } } return false; } /** * Generates a secret key for given hash function, returns RAW key or false * if function is not supported * * @param string $func hash function (sha1 or sha256) * @return mixed */ protected function _genSecret($func) { if ($func == 'sha1') { $macLen = 20; /* 160 bit */ } else if ($func == 'sha256') { $macLen = 32; /* 256 bit */ } else { return false; } return Zend_OpenId::randomBytes($macLen); } /** * Processes association request from OpenID consumerm generates secret * shared key and send it back using Diffie-Hellman encruption. * Returns array of variables to push back to consumer. * * @param float $version OpenID version * @param array $params GET or POST request variables * @return array */ protected function _associate($version, $params) { $ret = array(); if ($version >= 2.0) { $ret['ns'] = Zend_OpenId::NS_2_0; } if (isset($params['openid_assoc_type']) && $params['openid_assoc_type'] == 'HMAC-SHA1') { $macFunc = 'sha1'; } else if (isset($params['openid_assoc_type']) && $params['openid_assoc_type'] == 'HMAC-SHA256' && $version >= 2.0) { $macFunc = 'sha256'; } else { $ret['error'] = 'Wrong "openid.assoc_type"'; $ret['error-code'] = 'unsupported-type'; return $ret; } $ret['assoc_type'] = $params['openid_assoc_type']; $secret = $this->_genSecret($macFunc); if (empty($params['openid_session_type']) || $params['openid_session_type'] == 'no-encryption') { $ret['mac_key'] = base64_encode($secret); } else if (isset($params['openid_session_type']) && $params['openid_session_type'] == 'DH-SHA1') { $dhFunc = 'sha1'; } else if (isset($params['openid_session_type']) && $params['openid_session_type'] == 'DH-SHA256' && $version >= 2.0) { $dhFunc = 'sha256'; } else { $ret['error'] = 'Wrong "openid.session_type"'; $ret['error-code'] = 'unsupported-type'; return $ret; } if (isset($params['openid_session_type'])) { $ret['session_type'] = $params['openid_session_type']; } if (isset($dhFunc)) { if (empty($params['openid_dh_consumer_public'])) { $ret['error'] = 'Wrong "openid.dh_consumer_public"'; return $ret; } if (empty($params['openid_dh_gen'])) { $g = pack('H*', Zend_OpenId::DH_G); } else { $g = base64_decode($params['openid_dh_gen']); } if (empty($params['openid_dh_modulus'])) { $p = pack('H*', Zend_OpenId::DH_P); } else { $p = base64_decode($params['openid_dh_modulus']); } $dh = Zend_OpenId::createDhKey($p, $g); $dh_details = Zend_OpenId::getDhKeyDetails($dh); $sec = Zend_OpenId::computeDhSecret( base64_decode($params['openid_dh_consumer_public']), $dh); if ($sec === false) { $ret['error'] = 'Wrong "openid.session_type"'; $ret['error-code'] = 'unsupported-type'; return $ret; } $sec = Zend_OpenId::digest($dhFunc, $sec); $ret['dh_server_public'] = base64_encode( Zend_OpenId::btwoc($dh_details['pub_key'])); $ret['enc_mac_key'] = base64_encode($secret ^ $sec); } $handle = uniqid(); $expiresIn = $this->_sessionTtl; $ret['assoc_handle'] = $handle; $ret['expires_in'] = $expiresIn; $this->_storage->addAssociation($handle, $macFunc, $secret, time() + $expiresIn); return $ret; } /** * Performs authentication (or authentication check). * * @param float $version OpenID version * @param array $params GET or POST request variables * @param bool $immediate enables or disables interaction with user * @param mixed $extensions extension object or array of extensions objects * @param Zend_Controller_Response_Abstract $response * @return array */ protected function _checkId($version, $params, $immediate, $extensions=null, Zend_Controller_Response_Abstract $response = null) { $ret = array(); if ($version >= 2.0) { $ret['openid.ns'] = Zend_OpenId::NS_2_0; } $root = $this->getSiteRoot($params); if ($root === false) { return false; } if (isset($params['openid_identity']) && !$this->_storage->hasUser($params['openid_identity'])) { $ret['openid.mode'] = ($immediate && $version >= 2.0) ? 'setup_needed': 'cancel'; return $ret; } /* Check if user already logged in into the server */ if (!isset($params['openid_identity']) || $this->_user->getLoggedInUser() !== $params['openid_identity']) { $params2 = array(); foreach ($params as $key => $val) { if (strpos($key, 'openid_ns_') === 0) { $key = 'openid.ns.' . substr($key, strlen('openid_ns_')); } else if (strpos($key, 'openid_sreg_') === 0) { $key = 'openid.sreg.' . substr($key, strlen('openid_sreg_')); } else if (strpos($key, 'openid_') === 0) { $key = 'openid.' . substr($key, strlen('openid_')); } $params2[$key] = $val; } if ($immediate) { $params2['openid.mode'] = 'checkid_setup'; $ret['openid.mode'] = ($version >= 2.0) ? 'setup_needed': 'id_res'; $ret['openid.user_setup_url'] = $this->_loginUrl . (strpos($this->_loginUrl, '?') === false ? '?' : '&') . Zend_OpenId::paramsToQuery($params2); return $ret; } else { /* Redirect to Server Login Screen */ Zend_OpenId::redirect($this->_loginUrl, $params2, $response); return true; } } if (!Zend_OpenId_Extension::forAll($extensions, 'parseRequest', $params)) { $ret['openid.mode'] = ($immediate && $version >= 2.0) ? 'setup_needed': 'cancel'; return $ret; } /* Check if user trusts to the consumer */ $trusted = null; $sites = $this->_storage->getTrustedSites($params['openid_identity']); if (isset($params['openid_return_to'])) { $root = $params['openid_return_to']; } if (isset($sites[$root])) { $trusted = $sites[$root]; } else { foreach ($sites as $site => $t) { if (strpos($root, $site) === 0) { $trusted = $t; break; } else { /* OpenID 2.0 (9.2) check for realm wild-card matching */ $n = strpos($site, '://*.'); if ($n != false) { $regex = '/^' . preg_quote(substr($site, 0, $n+3), '/') . '[A-Za-z1-9_\.]+?' . preg_quote(substr($site, $n+4), '/') . '/'; if (preg_match($regex, $root)) { $trusted = $t; break; } } } } } if (is_array($trusted)) { if (!Zend_OpenId_Extension::forAll($extensions, 'checkTrustData', $trusted)) { $trusted = null; } } if ($trusted === false) { $ret['openid.mode'] = 'cancel'; return $ret; } else if ($trusted === null) { /* Redirect to Server Trust Screen */ $params2 = array(); foreach ($params as $key => $val) { if (strpos($key, 'openid_ns_') === 0) { $key = 'openid.ns.' . substr($key, strlen('openid_ns_')); } else if (strpos($key, 'openid_sreg_') === 0) { $key = 'openid.sreg.' . substr($key, strlen('openid_sreg_')); } else if (strpos($key, 'openid_') === 0) { $key = 'openid.' . substr($key, strlen('openid_')); } $params2[$key] = $val; } if ($immediate) { $params2['openid.mode'] = 'checkid_setup'; $ret['openid.mode'] = ($version >= 2.0) ? 'setup_needed': 'id_res'; $ret['openid.user_setup_url'] = $this->_trustUrl . (strpos($this->_trustUrl, '?') === false ? '?' : '&') . Zend_OpenId::paramsToQuery($params2); return $ret; } else { Zend_OpenId::redirect($this->_trustUrl, $params2, $response); return true; } } return $this->_respond($version, $ret, $params, $extensions); } /** * Perepares information to send back to consumer's authentication request, * signs it using shared secret and send back through HTTP redirection * * @param array $params GET or POST request variables * @param mixed $extensions extension object or array of extensions objects * @param Zend_Controller_Response_Abstract $response an optional response * object to perform HTTP or HTML form redirection * @return bool */ public function respondToConsumer($params, $extensions=null, Zend_Controller_Response_Abstract $response = null) { $version = 1.1; if (isset($params['openid_ns']) && $params['openid_ns'] == Zend_OpenId::NS_2_0) { $version = 2.0; } $ret = array(); if ($version >= 2.0) { $ret['openid.ns'] = Zend_OpenId::NS_2_0; } $ret = $this->_respond($version, $ret, $params, $extensions); if (!empty($params['openid_return_to'])) { Zend_OpenId::redirect($params['openid_return_to'], $ret, $response); } return true; } /** * Perepares information to send back to consumer's authentication request * and signs it using shared secret. * * @param float $version OpenID protcol version * @param array $ret arguments to be send back to consumer * @param array $params GET or POST request variables * @param mixed $extensions extension object or array of extensions objects * @return array */ protected function _respond($version, $ret, $params, $extensions=null) { if (empty($params['openid_assoc_handle']) || !$this->_storage->getAssociation($params['openid_assoc_handle'], $macFunc, $secret, $expires)) { /* Use dumb mode */ if (!empty($params['openid_assoc_handle'])) { $ret['openid.invalidate_handle'] = $params['openid_assoc_handle']; } $macFunc = $version >= 2.0 ? 'sha256' : 'sha1'; $secret = $this->_genSecret($macFunc); $handle = uniqid(); $expiresIn = $this->_sessionTtl; $this->_storage->addAssociation($handle, $macFunc, $secret, time() + $expiresIn); $ret['openid.assoc_handle'] = $handle; } else { $ret['openid.assoc_handle'] = $params['openid_assoc_handle']; } if (isset($params['openid_return_to'])) { $ret['openid.return_to'] = $params['openid_return_to']; } if (isset($params['openid_claimed_id'])) { $ret['openid.claimed_id'] = $params['openid_claimed_id']; } if (isset($params['openid_identity'])) { $ret['openid.identity'] = $params['openid_identity']; } if ($version >= 2.0) { if (!empty($this->_opEndpoint)) { $ret['openid.op_endpoint'] = $this->_opEndpoint; } else { $ret['openid.op_endpoint'] = Zend_OpenId::selfUrl(); } } $ret['openid.response_nonce'] = gmdate('Y-m-d\TH:i:s\Z') . uniqid(); $ret['openid.mode'] = 'id_res'; Zend_OpenId_Extension::forAll($extensions, 'prepareResponse', $ret); $signed = ''; $data = ''; foreach ($ret as $key => $val) { if (strpos($key, 'openid.') === 0) { $key = substr($key, strlen('openid.')); if (!empty($signed)) { $signed .= ','; } $signed .= $key; $data .= $key . ':' . $val . "\n"; } } $signed .= ',signed'; $data .= 'signed:' . $signed . "\n"; $ret['openid.signed'] = $signed; $ret['openid.sig'] = base64_encode( Zend_OpenId::hashHmac($macFunc, $data, $secret)); return $ret; } /** * Performs authentication validation for dumb consumers * Returns array of variables to push back to consumer. * It MUST contain 'is_valid' variable with value 'true' or 'false'. * * @param float $version OpenID version * @param array $params GET or POST request variables * @return array */ protected function _checkAuthentication($version, $params) { $ret = array(); if ($version >= 2.0) { $ret['ns'] = Zend_OpenId::NS_2_0; } $ret['openid.mode'] = 'id_res'; if (empty($params['openid_assoc_handle']) || empty($params['openid_signed']) || empty($params['openid_sig']) || !$this->_storage->getAssociation($params['openid_assoc_handle'], $macFunc, $secret, $expires)) { $ret['is_valid'] = 'false'; return $ret; } $signed = explode(',', $params['openid_signed']); $data = ''; foreach ($signed as $key) { $data .= $key . ':'; if ($key == 'mode') { $data .= "id_res\n"; } else { $data .= $params['openid_' . strtr($key,'.','_')]."\n"; } } if ($this->_secureStringCompare(base64_decode($params['openid_sig']), Zend_OpenId::hashHmac($macFunc, $data, $secret))) { $ret['is_valid'] = 'true'; } else { $ret['is_valid'] = 'false'; } return $ret; } /** * Securely compare two strings for equality while avoided C level memcmp() * optimisations capable of leaking timing information useful to an attacker * attempting to iteratively guess the unknown string (e.g. password) being * compared against. * * @param string $a * @param string $b * @return bool */ protected function _secureStringCompare($a, $b) { if (strlen($a) !== strlen($b)) { return false; } $result = 0; for ($i = 0; $i < strlen($a); $i++) { $result |= ord($a[$i]) ^ ord($b[$i]); } return $result == 0; } }