From 41eaa2df07cb902814b91d59fce742ccdc57af76 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Wed, 11 Dec 2013 13:56:14 +0200 Subject: [PATCH] Extension "authclient" recomposed. --- extensions/yii/authclient/BaseOAuth.php | 504 +++++++++++++ extensions/yii/authclient/OAuth1.php | 353 ++++++++++ extensions/yii/authclient/OAuth2.php | 184 +++++ extensions/yii/authclient/OAuthToken.php | 186 +++++ extensions/yii/authclient/OpenId.php | 780 +++++++++++++++++++++ extensions/yii/authclient/oauth/BaseClient.php | 504 ------------- extensions/yii/authclient/oauth/Client1.php | 353 ---------- extensions/yii/authclient/oauth/Client2.php | 184 ----- extensions/yii/authclient/oauth/Token.php | 186 ----- .../yii/authclient/oauth/signature/BaseMethod.php | 51 -- .../yii/authclient/oauth/signature/HmacSha1.php | 47 -- .../yii/authclient/oauth/signature/PlainText.php | 33 - .../yii/authclient/oauth/signature/RsaSha1.php | 168 ----- extensions/yii/authclient/openid/Client.php | 780 --------------------- extensions/yii/authclient/provider/OpenId.php | 30 + .../yii/authclient/provider/ProviderInterface.php | 73 ++ .../yii/authclient/provider/ProviderTrait.php | 176 +++++ extensions/yii/authclient/signature/BaseMethod.php | 51 ++ extensions/yii/authclient/signature/HmacSha1.php | 47 ++ extensions/yii/authclient/signature/PlainText.php | 33 + extensions/yii/authclient/signature/RsaSha1.php | 168 +++++ tests/unit/extensions/authclient/BaseOAuthTest.php | 251 +++++++ tests/unit/extensions/authclient/OAuth1Test.php | 109 +++ tests/unit/extensions/authclient/OAuth2Test.php | 33 + tests/unit/extensions/authclient/TokenTest.php | 133 ++++ .../extensions/authclient/oauth/BaseClientTest.php | 251 ------- .../extensions/authclient/oauth/Client1Test.php | 109 --- .../extensions/authclient/oauth/Client2Test.php | 33 - .../unit/extensions/authclient/oauth/TokenTest.php | 133 ---- .../authclient/oauth/signature/BaseMethodTest.php | 50 -- .../authclient/oauth/signature/HmacSha1Test.php | 20 - .../authclient/oauth/signature/PlainTextTest.php | 20 - .../authclient/oauth/signature/RsaSha1Test.php | 110 --- .../authclient/signature/BaseMethodTest.php | 50 ++ .../authclient/signature/HmacSha1Test.php | 20 + .../authclient/signature/PlainTextTest.php | 20 + .../authclient/signature/RsaSha1Test.php | 110 +++ 37 files changed, 3311 insertions(+), 3032 deletions(-) create mode 100644 extensions/yii/authclient/BaseOAuth.php create mode 100644 extensions/yii/authclient/OAuth1.php create mode 100644 extensions/yii/authclient/OAuth2.php create mode 100644 extensions/yii/authclient/OAuthToken.php create mode 100644 extensions/yii/authclient/OpenId.php delete mode 100644 extensions/yii/authclient/oauth/BaseClient.php delete mode 100644 extensions/yii/authclient/oauth/Client1.php delete mode 100644 extensions/yii/authclient/oauth/Client2.php delete mode 100644 extensions/yii/authclient/oauth/Token.php delete mode 100644 extensions/yii/authclient/oauth/signature/BaseMethod.php delete mode 100644 extensions/yii/authclient/oauth/signature/HmacSha1.php delete mode 100644 extensions/yii/authclient/oauth/signature/PlainText.php delete mode 100644 extensions/yii/authclient/oauth/signature/RsaSha1.php delete mode 100644 extensions/yii/authclient/openid/Client.php create mode 100644 extensions/yii/authclient/provider/OpenId.php create mode 100644 extensions/yii/authclient/provider/ProviderInterface.php create mode 100644 extensions/yii/authclient/provider/ProviderTrait.php create mode 100644 extensions/yii/authclient/signature/BaseMethod.php create mode 100644 extensions/yii/authclient/signature/HmacSha1.php create mode 100644 extensions/yii/authclient/signature/PlainText.php create mode 100644 extensions/yii/authclient/signature/RsaSha1.php create mode 100644 tests/unit/extensions/authclient/BaseOAuthTest.php create mode 100644 tests/unit/extensions/authclient/OAuth1Test.php create mode 100644 tests/unit/extensions/authclient/OAuth2Test.php create mode 100644 tests/unit/extensions/authclient/TokenTest.php delete mode 100644 tests/unit/extensions/authclient/oauth/BaseClientTest.php delete mode 100644 tests/unit/extensions/authclient/oauth/Client1Test.php delete mode 100644 tests/unit/extensions/authclient/oauth/Client2Test.php delete mode 100644 tests/unit/extensions/authclient/oauth/TokenTest.php delete mode 100644 tests/unit/extensions/authclient/oauth/signature/BaseMethodTest.php delete mode 100644 tests/unit/extensions/authclient/oauth/signature/HmacSha1Test.php delete mode 100644 tests/unit/extensions/authclient/oauth/signature/PlainTextTest.php delete mode 100644 tests/unit/extensions/authclient/oauth/signature/RsaSha1Test.php create mode 100644 tests/unit/extensions/authclient/signature/BaseMethodTest.php create mode 100644 tests/unit/extensions/authclient/signature/HmacSha1Test.php create mode 100644 tests/unit/extensions/authclient/signature/PlainTextTest.php create mode 100644 tests/unit/extensions/authclient/signature/RsaSha1Test.php diff --git a/extensions/yii/authclient/BaseOAuth.php b/extensions/yii/authclient/BaseOAuth.php new file mode 100644 index 0000000..1f02239 --- /dev/null +++ b/extensions/yii/authclient/BaseOAuth.php @@ -0,0 +1,504 @@ + + * @since 2.0 + */ +abstract class BaseOAuth extends Component +{ + const CONTENT_TYPE_JSON = 'json'; // JSON format + const CONTENT_TYPE_URLENCODED = 'urlencoded'; // urlencoded query string, like name1=value1&name2=value2 + const CONTENT_TYPE_XML = 'xml'; // XML format + const CONTENT_TYPE_AUTO = 'auto'; // attempts to determine format automatically + + /** + * @var string protocol version. + */ + public $version = '1.0'; + /** + * @var string URL, which user will be redirected after authentication at the OAuth provider web site. + * Note: this should be absolute URL (with http:// or https:// leading). + * By default current URL will be used. + */ + private $_returnUrl = ''; + /** + * @var string API base URL. + */ + public $apiBaseUrl = ''; + /** + * @var string authorize URL. + */ + public $authUrl = ''; + /** + * @var string auth request scope. + */ + public $scope = ''; + /** + * @var array cURL request options. Option values from this field will overwrite corresponding + * values from {@link defaultCurlOptions()}. + */ + private $_curlOptions = []; + /** + * @var OAuthToken|array access token instance or its array configuration. + */ + private $_accessToken = null; + /** + * @var signature\BaseMethod|array signature method instance or its array configuration. + */ + private $_signatureMethod = []; + + /** + * @param string $returnUrl return URL + */ + public function setReturnUrl($returnUrl) + { + $this->_returnUrl = $returnUrl; + } + + /** + * @return string return URL. + */ + public function getReturnUrl() + { + if (empty($this->_returnUrl)) { + $this->_returnUrl = $this->defaultReturnUrl(); + } + return $this->_returnUrl; + } + + /** + * @param array $curlOptions cURL options. + */ + public function setCurlOptions(array $curlOptions) + { + $this->_curlOptions = $curlOptions; + } + + /** + * @return array cURL options. + */ + public function getCurlOptions() + { + return $this->_curlOptions; + } + + /** + * @param array|OAuthToken $token + */ + public function setAccessToken($token) + { + if (!is_object($token)) { + $token = $this->createToken($token); + } + $this->_accessToken = $token; + $this->saveAccessToken($token); + } + + /** + * @return OAuthToken auth token instance. + */ + public function getAccessToken() + { + if (!is_object($this->_accessToken)) { + $this->_accessToken = $this->restoreAccessToken(); + } + return $this->_accessToken; + } + + /** + * @param array|signature\BaseMethod $signatureMethod signature method instance or its array configuration. + * @throws InvalidParamException on wrong argument. + */ + public function setSignatureMethod($signatureMethod) + { + if (!is_object($signatureMethod) && !is_array($signatureMethod)) { + throw new InvalidParamException('"' . get_class($this) . '::signatureMethod" should be instance of "\yii\autclient\signature\BaseMethod" or its array configuration. "' . gettype($signatureMethod) . '" has been given.'); + } + $this->_signatureMethod = $signatureMethod; + } + + /** + * @return signature\BaseMethod signature method instance. + */ + public function getSignatureMethod() + { + if (!is_object($this->_signatureMethod)) { + $this->_signatureMethod = $this->createSignatureMethod($this->_signatureMethod); + } + return $this->_signatureMethod; + } + + /** + * Composes default {@link returnUrl} value. + * @return string return URL. + */ + protected function defaultReturnUrl() + { + return Yii::$app->getRequest()->getAbsoluteUrl(); + } + + /** + * Sends HTTP request. + * @param string $method request type. + * @param string $url request URL. + * @param array $params request params. + * @return array response. + * @throws Exception on failure. + */ + protected function sendRequest($method, $url, array $params = []) + { + $curlOptions = $this->mergeCurlOptions( + $this->defaultCurlOptions(), + $this->getCurlOptions(), + array( + CURLOPT_RETURNTRANSFER => true, + CURLOPT_URL => $url, + ), + $this->composeRequestCurlOptions(strtoupper($method), $url, $params) + ); + $curlResource = curl_init(); + foreach ($curlOptions as $option => $value) { + curl_setopt($curlResource, $option, $value); + } + $response = curl_exec($curlResource); + $responseHeaders = curl_getinfo($curlResource); + + // check cURL error + $errorNumber = curl_errno($curlResource); + $errorMessage = curl_error($curlResource); + + curl_close($curlResource); + + if ($errorNumber > 0) { + throw new Exception('Curl error requesting "' . $url . '": #' . $errorNumber . ' - ' . $errorMessage); + } + if ($responseHeaders['http_code'] != 200) { + throw new Exception('Request failed with code: ' . $responseHeaders['http_code'] . ', message: ' . $response); + } + return $this->processResponse($response, $this->determineContentTypeByHeaders($responseHeaders)); + } + + /** + * Merge CUrl options. + * If each options array has an element with the same key value, the latter + * will overwrite the former. + * @param array $options1 options to be merged to. + * @param array $options2 options to be merged from. You can specify additional + * arrays via third argument, fourth argument etc. + * @return array merged options (the original options are not changed.) + */ + protected function mergeCurlOptions($options1, $options2) + { + $args = func_get_args(); + $res = array_shift($args); + while (!empty($args)) { + $next = array_shift($args); + foreach ($next as $k => $v) { + $res[$k]=$v; + } + } + return $res; + } + + /** + * Returns default cURL options. + * @return array cURL options. + */ + protected function defaultCurlOptions() + { + return [ + CURLOPT_USERAGENT => Yii::$app->name . ' OAuth ' . $this->version . ' Client', + CURLOPT_CONNECTTIMEOUT => 30, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => false, + ]; + } + + /** + * Processes raw response converting it to actual data. + * @param string $rawResponse raw response. + * @param string $contentType response content type. + * @throws Exception on failure. + * @return array actual response. + */ + protected function processResponse($rawResponse, $contentType = self::CONTENT_TYPE_AUTO) + { + if (empty($rawResponse)) { + return []; + } + switch ($contentType) { + case self::CONTENT_TYPE_AUTO: { + $contentType = $this->determineContentTypeByRaw($rawResponse); + if ($contentType == self::CONTENT_TYPE_AUTO) { + throw new Exception('Unable to determine response content type automatically.'); + } + $response = $this->processResponse($rawResponse, $contentType); + break; + } + case self::CONTENT_TYPE_JSON: { + $response = Json::decode($rawResponse, true); + if (isset($response['error'])) { + throw new Exception('Response error: ' . $response['error']); + } + break; + } + case self::CONTENT_TYPE_URLENCODED: { + $response = []; + parse_url($rawResponse, $response); + break; + } + case self::CONTENT_TYPE_XML: { + $response = $this->convertXmlToArray($rawResponse); + break; + } + default: { + throw new Exception('Unknown response type "' . $contentType . '".'); + } + } + return $response; + } + + /** + * Converts XML document to array. + * @param string|\SimpleXMLElement $xml xml to process. + * @return array XML array representation. + */ + protected function convertXmlToArray($xml) + { + if (!is_object($xml)) { + $xml = simplexml_load_string($xml); + } + $result = (array)$xml; + foreach ($result as $key => $value) { + if (is_object($value)) { + $result[$key] = $this->convertXmlToArray($value); + } + } + return $result; + } + + /** + * Attempts to determine HTTP request content type by headers. + * @param array $headers request headers. + * @return string content type. + */ + protected function determineContentTypeByHeaders(array $headers) + { + if (isset($headers['content_type'])) { + if (stripos($headers['content_type'], 'json') !== false) { + return self::CONTENT_TYPE_JSON; + } + if (stripos($headers['content_type'], 'urlencoded') !== false) { + return self::CONTENT_TYPE_URLENCODED; + } + if (stripos($headers['content_type'], 'xml') !== false) { + return self::CONTENT_TYPE_XML; + } + } + return self::CONTENT_TYPE_AUTO; + } + + /** + * Attempts to determine the content type from raw content. + * @param string $rawContent raw response content. + * @return string response type. + */ + protected function determineContentTypeByRaw($rawContent) + { + if (preg_match('/^\\{.*\\}$/is', $rawContent)) { + return self::CONTENT_TYPE_JSON; + } + if (preg_match('/^[^=|^&]+=[^=|^&]+(&[^=|^&]+=[^=|^&]+)*$/is', $rawContent)) { + return self::CONTENT_TYPE_URLENCODED; + } + if (preg_match('/^<.*>$/is', $rawContent)) { + return self::CONTENT_TYPE_XML; + } + return self::CONTENT_TYPE_AUTO; + } + + /** + * Creates signature method instance from its configuration. + * @param array $signatureMethodConfig signature method configuration. + * @return signature\BaseMethod signature method instance. + */ + protected function createSignatureMethod(array $signatureMethodConfig) + { + if (!array_key_exists('class', $signatureMethodConfig)) { + $signatureMethodConfig['class'] = signature\HmacSha1::className(); + } + return Yii::createObject($signatureMethodConfig); + } + + /** + * Creates token from its configuration. + * @param array $tokenConfig token configuration. + * @return OAuthToken token instance. + */ + protected function createToken(array $tokenConfig = []) + { + if (!array_key_exists('class', $tokenConfig)) { + $tokenConfig['class'] = OAuthToken::className(); + } + return Yii::createObject($tokenConfig); + } + + /** + * Composes URL from base URL and GET params. + * @param string $url base URL. + * @param array $params GET params. + * @return string composed URL. + */ + protected function composeUrl($url, array $params = []) + { + if (strpos($url, '?') === false) { + $url .= '?'; + } else { + $url .= '&'; + } + $url .= http_build_query($params, '', '&', PHP_QUERY_RFC3986); + return $url; + } + + /** + * Saves token as persistent state. + * @param OAuthToken $token auth token + * @return static self reference. + */ + protected function saveAccessToken(OAuthToken $token) + { + return $this->setState('token', $token); + } + + /** + * Restores access token. + * @return OAuthToken auth token. + */ + protected function restoreAccessToken() + { + $token = $this->getState('token'); + if (is_object($token)) { + /* @var $token OAuthToken */ + if ($token->getIsExpired()) { + $token = $this->refreshAccessToken($token); + } + } + return $token; + } + + /** + * Sets persistent state. + * @param string $key state key. + * @param mixed $value state value + * @return static self reference. + */ + protected function setState($key, $value) + { + $session = Yii::$app->getSession(); + $key = $this->getStateKeyPrefix() . $key; + $session->set($key, $value); + return $this; + } + + /** + * Returns persistent state value. + * @param string $key state key. + * @return mixed state value. + */ + protected function getState($key) + { + $session = Yii::$app->getSession(); + $key = $this->getStateKeyPrefix() . $key; + $value = $session->get($key); + return $value; + } + + /** + * Removes persistent state value. + * @param string $key state key. + * @return boolean success. + */ + protected function removeState($key) + { + $session = Yii::$app->getSession(); + $key = $this->getStateKeyPrefix() . $key; + $session->remove($key); + return true; + } + + /** + * Returns session key prefix, which is used to store internal states. + * @return string session key prefix. + */ + protected function getStateKeyPrefix() + { + return get_class($this) . '_' . sha1($this->authUrl) . '_'; + } + + /** + * Performs request to the OAuth API. + * @param string $apiSubUrl API sub URL, which will be append to [[apiBaseUrl]], or absolute API URL. + * @param string $method request method. + * @param array $params request parameters. + * @return array API response + * @throws Exception on failure. + */ + public function api($apiSubUrl, $method = 'GET', array $params = []) + { + if (preg_match('/^https?:\\/\\//is', $apiSubUrl)) { + $url = $apiSubUrl; + } else { + $url = $this->apiBaseUrl . '/' . $apiSubUrl; + } + $accessToken = $this->getAccessToken(); + if (!is_object($accessToken) || !$accessToken->getIsValid()) { + throw new Exception('Invalid access token.'); + } + return $this->apiInternal($accessToken, $url, $method, $params); + } + + /** + * Composes HTTP request CUrl options, which will be merged with the default ones. + * @param string $method request type. + * @param string $url request URL. + * @param array $params request params. + * @return array CUrl options. + * @throws Exception on failure. + */ + abstract protected function composeRequestCurlOptions($method, $url, array $params); + + /** + * Gets new auth token to replace expired one. + * @param OAuthToken $token expired auth token. + * @return OAuthToken new auth token. + */ + abstract public function refreshAccessToken(OAuthToken $token); + + /** + * Performs request to the OAuth API. + * @param OAuthToken $accessToken actual access token. + * @param string $url absolute API URL. + * @param string $method request method. + * @param array $params request parameters. + * @return array API response. + * @throws Exception on failure. + */ + abstract protected function apiInternal($accessToken, $url, $method, array $params); +} \ No newline at end of file diff --git a/extensions/yii/authclient/OAuth1.php b/extensions/yii/authclient/OAuth1.php new file mode 100644 index 0000000..11c99e8 --- /dev/null +++ b/extensions/yii/authclient/OAuth1.php @@ -0,0 +1,353 @@ +fetchRequestToken(); // Get request token + * $url = $oauthClient->buildAuthUrl($requestToken); // Get authorization URL + * Yii::$app->getResponse()->redirect($url); // Redirect to authorization URL + * // After user returns at our site: + * $accessToken = $oauthClient->fetchAccessToken($requestToken); // Upgrade to access token + * ~~~ + * + * @see http://oauth.net/ + * + * @author Paul Klimov + * @since 2.0 + */ +class OAuth1 extends BaseOAuth +{ + /** + * @var string protocol version. + */ + public $version = '1.0'; + /** + * @var string OAuth consumer key. + */ + public $consumerKey = ''; + /** + * @var string OAuth consumer secret. + */ + public $consumerSecret = ''; + /** + * @var string OAuth request token URL. + */ + public $requestTokenUrl = ''; + /** + * @var string request token HTTP method. + */ + public $requestTokenMethod = 'GET'; + /** + * @var string OAuth access token URL. + */ + public $accessTokenUrl = ''; + /** + * @var string access token HTTP method. + */ + public $accessTokenMethod = 'GET'; + + /** + * Fetches the OAuth request token. + * @param array $params additional request params. + * @return OAuthToken request token. + */ + public function fetchRequestToken(array $params = []) + { + $this->removeState('token'); + $defaultParams = [ + 'oauth_consumer_key' => $this->consumerKey, + 'oauth_callback' => $this->getReturnUrl(), + //'xoauth_displayname' => Yii::$app->name, + ]; + if (!empty($this->scope)) { + $defaultParams['scope'] = $this->scope; + } + $response = $this->sendSignedRequest($this->requestTokenMethod, $this->requestTokenUrl, array_merge($defaultParams, $params)); + $token = $this->createToken([ + 'params' => $response + ]); + $this->setState('requestToken', $token); + return $token; + } + + /** + * Composes user authorization URL. + * @param OAuthToken $requestToken OAuth request token. + * @param array $params additional request params. + * @return string authorize URL + * @throws Exception on failure. + */ + public function buildAuthUrl(OAuthToken $requestToken = null, array $params = []) + { + if (!is_object($requestToken)) { + $requestToken = $this->getState('requestToken'); + if (!is_object($requestToken)) { + throw new Exception('Request token is required to build authorize URL!'); + } + } + $params['oauth_token'] = $requestToken->getToken(); + return $this->composeUrl($this->authUrl, $params); + } + + /** + * Fetches OAuth access token. + * @param OAuthToken $requestToken OAuth request token. + * @param string $oauthVerifier OAuth verifier. + * @param array $params additional request params. + * @return OAuthToken OAuth access token. + * @throws Exception on failure. + */ + public function fetchAccessToken(OAuthToken $requestToken = null, $oauthVerifier = null, array $params = []) + { + if (!is_object($requestToken)) { + $requestToken = $this->getState('requestToken'); + if (!is_object($requestToken)) { + throw new Exception('Request token is required to fetch access token!'); + } + } + $this->removeState('requestToken'); + $defaultParams = [ + 'oauth_consumer_key' => $this->consumerKey, + 'oauth_token' => $requestToken->getToken() + ]; + if ($oauthVerifier === null) { + if (isset($_REQUEST['oauth_verifier'])) { + $oauthVerifier = $_REQUEST['oauth_verifier']; + } + } + if (!empty($oauthVerifier)) { + $defaultParams['oauth_verifier'] = $oauthVerifier; + } + $response = $this->sendSignedRequest($this->accessTokenMethod, $this->accessTokenUrl, array_merge($defaultParams, $params)); + + $token = $this->createToken([ + 'params' => $response + ]); + $this->setAccessToken($token); + return $token; + } + + /** + * Sends HTTP request, signed by {@link signatureMethod}. + * @param string $method request type. + * @param string $url request URL. + * @param array $params request params. + * @return array response. + */ + protected function sendSignedRequest($method, $url, array $params = []) + { + $params = array_merge($params, $this->generateCommonRequestParams()); + $params = $this->signRequest($method, $url, $params); + return $this->sendRequest($method, $url, $params); + } + + /** + * Composes HTTP request CUrl options, which will be merged with the default ones. + * @param string $method request type. + * @param string $url request URL. + * @param array $params request params. + * @return array CUrl options. + * @throws Exception on failure. + */ + protected function composeRequestCurlOptions($method, $url, array $params) + { + $curlOptions = []; + switch ($method) { + case 'GET': { + $curlOptions[CURLOPT_URL] = $this->composeUrl($url, $params); + break; + } + case 'POST': { + $curlOptions[CURLOPT_POST] = true; + if (!empty($params)){ + $curlOptions[CURLOPT_POSTFIELDS] = $params; + } + $authorizationHeader = $this->composeAuthorizationHeader($params); + if (!empty($authorizationHeader)/* && $this->curlAuthHeader*/) { + $curlOptions[CURLOPT_HTTPHEADER] = ['Content-Type: application/atom+xml', $authorizationHeader]; + } + break; + } + case 'HEAD': + case 'PUT': + case 'DELETE': { + $curlOptions[CURLOPT_CUSTOMREQUEST] = $method; + if (!empty($params)) { + $curlOptions[CURLOPT_URL] = $this->composeUrl($url, $params); + } + break; + } + default: { + throw new Exception("Unknown request method '{$method}'."); + } + } + return $curlOptions; + } + + /** + * Performs request to the OAuth API. + * @param OAuthToken $accessToken actual access token. + * @param string $url absolute API URL. + * @param string $method request method. + * @param array $params request parameters. + * @return array API response. + * @throws Exception on failure. + */ + protected function apiInternal($accessToken, $url, $method, array $params) + { + $params['oauth_consumer_key'] = $this->consumerKey; + $params['oauth_token'] = $accessToken->getToken(); + $response = $this->sendSignedRequest($method, $url, $params); + return $response; + } + + /** + * Gets new auth token to replace expired one. + * @param OAuthToken $token expired auth token. + * @return OAuthToken new auth token. + */ + public function refreshAccessToken(OAuthToken $token) + { + // @todo + return null; + } + + /** + * Composes default {@link returnUrl} value. + * @return string return URL. + */ + protected function defaultReturnUrl() + { + $params = $_GET; + unset($params['oauth_token']); + return Yii::$app->getUrlManager()->createAbsoluteUrl(Yii::$app->controller->getRoute(), $params); + } + + /** + * Generates nonce value. + * @return string nonce value. + */ + protected function generateNonce() + { + return md5(microtime() . mt_rand()); + } + + /** + * Generates timestamp. + * @return integer timestamp. + */ + protected function generateTimestamp() + { + return time(); + } + + /** + * Generate common request params like version, timestamp etc. + * @return array common request params. + */ + protected function generateCommonRequestParams() + { + $params = [ + 'oauth_version' => $this->version, + 'oauth_nonce' => $this->generateNonce(), + 'oauth_timestamp' => $this->generateTimestamp(), + ]; + return $params; + } + + /** + * Sign request with {@link signatureMethod}. + * @param string $method request method. + * @param string $url request URL. + * @param array $params request params. + * @return array signed request params. + */ + protected function signRequest($method, $url, array $params) + { + $signatureMethod = $this->getSignatureMethod(); + $params['oauth_signature_method'] = $signatureMethod->getName(); + $signatureBaseString = $this->composeSignatureBaseString($method, $url, $params); + $signatureKey = $this->composeSignatureKey(); + $params['oauth_signature'] = $signatureMethod->generateSignature($signatureBaseString, $signatureKey); + return $params; + } + + /** + * Creates signature base string, which will be signed by {@link signatureMethod}. + * @param string $method request method. + * @param string $url request URL. + * @param array $params request params. + * @return string base signature string. + */ + protected function composeSignatureBaseString($method, $url, array $params) + { + unset($params['oauth_signature']); + $parts = [ + strtoupper($method), + $url, + http_build_query($params, '', '&', PHP_QUERY_RFC3986) + ]; + $parts = array_map('rawurlencode', $parts); + return implode('&', $parts); + } + + /** + * Composes request signature key. + * @return string signature key. + */ + protected function composeSignatureKey() + { + $signatureKeyParts = [ + $this->consumerSecret + ]; + $accessToken = $this->getAccessToken(); + if (is_object($accessToken)) { + $signatureKeyParts[] = $accessToken->getTokenSecret(); + } else { + $signatureKeyParts[] = ''; + } + $signatureKeyParts = array_map('rawurlencode', $signatureKeyParts); + return implode('&', $signatureKeyParts); + } + + /** + * Composes authorization header content. + * @param array $params request params. + * @param string $realm authorization realm. + * @return string authorization header content. + */ + protected function composeAuthorizationHeader(array $params, $realm = '') + { + $header = 'Authorization: OAuth'; + $headerParams = []; + if (!empty($realm)) { + $headerParams[] = 'realm="' . rawurlencode($realm) . '"'; + } + foreach ($params as $key => $value) { + if (substr($key, 0, 5) != 'oauth') { + continue; + } + $headerParams[] = rawurlencode($key) . '="' . rawurlencode($value) . '"'; + } + if (!empty($headerParams)) { + $header .= ' ' . implode(', ', $headerParams); + } + return $header; + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/OAuth2.php b/extensions/yii/authclient/OAuth2.php new file mode 100644 index 0000000..b6e4368 --- /dev/null +++ b/extensions/yii/authclient/OAuth2.php @@ -0,0 +1,184 @@ +buildAuthUrl(); // Build authorization URL + * Yii::$app->getResponse()->redirect($url); // Redirect to authorization URL. + * // After user returns at our site: + * $code = $_GET['code']; + * $accessToken = $oauthClient->fetchAccessToken($code); // Get access token + * ~~~ + * + * @see http://oauth.net/2/ + * + * @author Paul Klimov + * @since 2.0 + */ +class OAuth2 extends BaseOAuth +{ + /** + * @var string protocol version. + */ + public $version = '2.0'; + /** + * @var string OAuth client ID. + */ + public $clientId = ''; + /** + * @var string OAuth client secret. + */ + public $clientSecret = ''; + /** + * @var string token request URL endpoint. + */ + public $tokenUrl = ''; + + /** + * Composes user authorization URL. + * @param array $params additional auth GET params. + * @return string authorization URL. + */ + public function buildAuthUrl(array $params = []) + { + $defaultParams = [ + 'client_id' => $this->clientId, + 'response_type' => 'code', + 'redirect_uri' => $this->getReturnUrl(), + 'xoauth_displayname' => Yii::$app->name, + ]; + if (!empty($this->scope)) { + $defaultParams['scope'] = $this->scope; + } + return $this->composeUrl($this->authUrl, array_merge($defaultParams, $params)); + } + + /** + * Fetches access token from authorization code. + * @param string $authCode authorization code, usually comes at $_GET['code']. + * @param array $params additional request params. + * @return OAuthToken access token. + */ + public function fetchAccessToken($authCode, array $params = []) + { + $defaultParams = [ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'code' => $authCode, + 'grant_type' => 'authorization_code', + 'redirect_uri' => $this->getReturnUrl(), + ]; + $response = $this->sendRequest('POST', $this->tokenUrl, array_merge($defaultParams, $params)); + $token = $this->createToken(['params' => $response]); + $this->setAccessToken($token); + return $token; + } + + /** + * Composes HTTP request CUrl options, which will be merged with the default ones. + * @param string $method request type. + * @param string $url request URL. + * @param array $params request params. + * @return array CUrl options. + * @throws Exception on failure. + */ + protected function composeRequestCurlOptions($method, $url, array $params) + { + $curlOptions = []; + switch ($method) { + case 'GET': { + $curlOptions[CURLOPT_URL] = $this->composeUrl($url, $params); + break; + } + case 'POST': { + $curlOptions[CURLOPT_POST] = true; + $curlOptions[CURLOPT_HTTPHEADER] = ['Content-type: application/x-www-form-urlencoded']; + $curlOptions[CURLOPT_POSTFIELDS] = http_build_query($params, '', '&', PHP_QUERY_RFC3986); + break; + } + case 'HEAD': + case 'PUT': + case 'DELETE': { + $curlOptions[CURLOPT_CUSTOMREQUEST] = $method; + if (!empty($params)) { + $curlOptions[CURLOPT_URL] = $this->composeUrl($url, $params); + } + break; + } + default: { + throw new Exception("Unknown request method '{$method}'."); + } + } + return $curlOptions; + } + + /** + * Performs request to the OAuth API. + * @param OAuthToken $accessToken actual access token. + * @param string $url absolute API URL. + * @param string $method request method. + * @param array $params request parameters. + * @return array API response. + * @throws Exception on failure. + */ + protected function apiInternal($accessToken, $url, $method, array $params) + { + $params['access_token'] = $accessToken->getToken(); + return $this->sendRequest($method, $url, $params); + } + + /** + * Gets new auth token to replace expired one. + * @param OAuthToken $token expired auth token. + * @return OAuthToken new auth token. + */ + public function refreshAccessToken(OAuthToken $token) + { + $params = [ + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'grant_type' => 'refresh_token' + ]; + $params = array_merge($token->getParams(), $params); + $response = $this->sendRequest('POST', $this->tokenUrl, $params); + return $response; + } + + /** + * Composes default {@link returnUrl} value. + * @return string return URL. + */ + protected function defaultReturnUrl() + { + $params = $_GET; + unset($params['code']); + return Yii::$app->getUrlManager()->createAbsoluteUrl(Yii::$app->controller->getRoute(), $params); + } + + /** + * Creates token from its configuration. + * @param array $tokenConfig token configuration. + * @return OAuthToken token instance. + */ + protected function createToken(array $tokenConfig = []) + { + $tokenConfig['tokenParamKey'] = 'access_token'; + return parent::createToken($tokenConfig); + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/OAuthToken.php b/extensions/yii/authclient/OAuthToken.php new file mode 100644 index 0000000..2f99559 --- /dev/null +++ b/extensions/yii/authclient/OAuthToken.php @@ -0,0 +1,186 @@ + + * @since 2.0 + */ +class OAuthToken extends Object +{ + /** + * @var string key in {@link _params} array, which stores token key. + */ + public $tokenParamKey = 'oauth_token'; + /** + * @var string key in {@link _params} array, which stores token secret key. + */ + public $tokenSecretParamKey = 'oauth_token_secret'; + /** + * @var string key in {@link _params} array, which stores token expiration duration. + * If not set will attempt to fetch its value automatically. + */ + private $_expireDurationParamKey; + /** + * @var array token parameters. + */ + private $_params = []; + /** + * @var integer object creation timestamp. + */ + public $createTimestamp; + + public function init() + { + if ($this->createTimestamp === null) { + $this->createTimestamp = time(); + } + } + + /** + * @param string $expireDurationParamKey expire duration param key. + */ + public function setExpireDurationParamKey($expireDurationParamKey) { + $this->_expireDurationParamKey = $expireDurationParamKey; + } + + /** + * @return string expire duration param key. + */ + public function getExpireDurationParamKey() { + if ($this->_expireDurationParamKey === null) { + $this->_expireDurationParamKey = $this->defaultExpireDurationParamKey(); + } + return $this->_expireDurationParamKey; + } + + /** + * @return array + */ + public function getParams() { + return $this->_params; + } + + /** + * @param array $params + */ + public function setParams(array $params) { + $this->_params = $params; + } + + /** + * Sets param by name. + * @param string $name param name. + * @param mixed $value param value, + */ + public function setParam($name, $value) { + $this->_params[$name] = $value; + } + + /** + * Returns param by name. + * @param string $name param name. + * @return mixed param value. + */ + public function getParam($name) { + return isset($this->_params[$name]) ? $this->_params[$name] : null; + } + + /** + * Sets token value. + * @param string $token token value. + * @return static self reference. + */ + public function setToken($token) { + $this->setParam($this->tokenParamKey, $token); + } + + /** + * Returns token value. + * @return string token value. + */ + public function getToken() { + return $this->getParam($this->tokenParamKey); + } + + /** + * Sets the token secret value. + * @param string $tokenSecret token secret. + */ + public function setTokenSecret($tokenSecret) { + $this->setParam($this->tokenSecretParamKey, $tokenSecret); + } + + /** + * Returns the token secret value. + * @return string token secret value. + */ + public function getTokenSecret() { + return $this->getParam($this->tokenSecretParamKey); + } + + /** + * Sets token expire duration. + * @param string $expireDuration token expiration duration. + */ + public function setExpireDuration($expireDuration) { + $this->setParam($this->getExpireDurationParamKey(), $expireDuration); + } + + /** + * Returns the token expiration duration. + * @return integer token expiration duration. + */ + public function getExpireDuration() { + return $this->getParam($this->getExpireDurationParamKey()); + } + + /** + * Fetches default expire duration param key. + * @return string expire duration param key. + */ + protected function defaultExpireDurationParamKey() { + $expireDurationParamKey = 'expires_in'; + foreach ($this->getParams() as $name => $value) { + if (strpos($name, 'expir') !== false) { + $expireDurationParamKey = $name; + break; + } + } + return $expireDurationParamKey; + } + + /** + * Checks if token has expired. + * @return boolean is token expired. + */ + public function getIsExpired() { + $expirationDuration = $this->getExpireDuration(); + if (empty($expirationDuration)) { + return false; + } + return (time() >= ($this->createTimestamp + $expirationDuration)); + } + + /** + * Checks if token is valid. + * @return boolean is token valid. + */ + public function getIsValid() { + $token = $this->getToken(); + return (!empty($token) && !$this->getIsExpired()); + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/OpenId.php b/extensions/yii/authclient/OpenId.php new file mode 100644 index 0000000..f421379 --- /dev/null +++ b/extensions/yii/authclient/OpenId.php @@ -0,0 +1,780 @@ + + * @since 2.0 + */ +class OpenId extends Component +{ + public $required = []; + public $optional = []; + public $verify_peer; + public $capath; + public $cainfo; + + private $_returnUrl; + private $_identity; + private $claimed_id; + private $_trustRoot; + + protected $server; + protected $version; + + protected $aliases; + protected $identifier_select = false; + protected $ax = false; + protected $sreg = false; + protected $data; + + public static $axToSregMap = [ + 'namePerson/friendly' => 'nickname', + 'contact/email' => 'email', + 'namePerson' => 'fullname', + 'birthDate' => 'dob', + 'person/gender' => 'gender', + 'contact/postalCode/home' => 'postcode', + 'contact/country/home' => 'country', + 'pref/language' => 'language', + 'pref/timezone' => 'timezone', + ]; + + /** + * @inheritdoc + */ + public function init() + { + $this->data = $_POST + $_GET; # OPs may send data as POST or GET. + } + + public function setIdentity($value) + { + if (strlen($value = trim((String) $value))) { + if (preg_match('#^xri:/*#i', $value, $m)) { + $value = substr($value, strlen($m[0])); + } elseif (!preg_match('/^(?:[=@+\$!\(]|https?:)/i', $value)) { + $value = "http://$value"; + } + if (preg_match('#^https?://[^/]+$#i', $value, $m)) { + $value .= '/'; + } + } + $this->_identity = $value; + $this->claimed_id = $value; + } + + public function setReturnUrl($returnUrl) + { + $this->_returnUrl = $returnUrl; + } + + public function getReturnUrl() + { + if ($this->_returnUrl === null) { + $uri = rtrim(preg_replace('#((?<=\?)|&)openid\.[^&]+#', '', $_SERVER['REQUEST_URI']), '?'); + $this->_returnUrl = $this->getTrustRoot() . $uri; + } + return $this->_returnUrl; + } + + public function getIdentity() + { + # We return claimed_id instead of identity, + # because the developer should see the claimed identifier, + # i.e. what he set as identity, not the op-local identifier (which is what we verify) + return $this->claimed_id; + } + + public function setTrustRoot($value) + { + $this->_trustRoot = trim($value); + } + + public function getTrustRoot() + { + if ($this->_trustRoot === null) { + $this->_trustRoot = (!empty($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST']; + } + return $this->_trustRoot; + } + + public function setRealm($value) + { + $this->setTrustRoot($value); + } + + public function getRealm() + { + return $this->getTrustRoot(); + } + + public function getMode() + { + return empty($this->data['openid_mode']) ? null : $this->data['openid_mode']; + } + + /** + * Checks if the server specified in the url exists. + * @param string $url URL to check + * @return boolean true, if the server exists; false otherwise + */ + public function hostExists($url) + { + if (strpos($url, '/') === false) { + $server = $url; + } else { + $server = @parse_url($url, PHP_URL_HOST); + } + if (!$server) { + return false; + } + $ips = gethostbynamel($server); + return !empty($ips); + } + + protected function sendCurlRequest($url, $method = 'GET', $params = []) + { + $params = http_build_query($params, '', '&'); + $curl = curl_init($url . ($method == 'GET' && $params ? '?' . $params : '')); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($curl, CURLOPT_HEADER, false); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: application/xrds+xml, */*')); + + if ($this->verify_peer !== null) { + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verify_peer); + if($this->capath) { + curl_setopt($curl, CURLOPT_CAPATH, $this->capath); + } + if($this->cainfo) { + curl_setopt($curl, CURLOPT_CAINFO, $this->cainfo); + } + } + + if ($method == 'POST') { + curl_setopt($curl, CURLOPT_POST, true); + curl_setopt($curl, CURLOPT_POSTFIELDS, $params); + } elseif ($method == 'HEAD') { + curl_setopt($curl, CURLOPT_HEADER, true); + curl_setopt($curl, CURLOPT_NOBODY, true); + } else { + curl_setopt($curl, CURLOPT_HTTPGET, true); + } + $response = curl_exec($curl); + + if ($method == 'HEAD') { + $headers = []; + foreach (explode("\n", $response) as $header) { + $pos = strpos($header,':'); + $name = strtolower(trim(substr($header, 0, $pos))); + $headers[$name] = trim(substr($header, $pos+1)); + } + + # Updating claimed_id in case of redirections. + $effective_url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL); + if ($effective_url != $url) { + $this->identity = $this->claimed_id = $effective_url; + } + + return $headers; + } + + if (curl_errno($curl)) { + throw new Exception(curl_error($curl), curl_errno($curl)); + } + + return $response; + } + + protected function sendStreamRequest($url, $method = 'GET', $params = []) + { + if (!$this->hostExists($url)) { + throw new Exception('Invalid request.'); + } + + $params = http_build_query($params, '', '&'); + switch ($method) { + case 'GET': + $options = [ + 'http' => [ + 'method' => 'GET', + 'header' => 'Accept: application/xrds+xml, */*', + 'ignore_errors' => true, + ] + ]; + $url = $url . ($params ? '?' . $params : ''); + break; + case 'POST': + $options = [ + 'http' => [ + 'method' => 'POST', + 'header' => 'Content-type: application/x-www-form-urlencoded', + 'content' => $params, + 'ignore_errors' => true, + ] + ]; + break; + case 'HEAD': + # We want to send a HEAD request, + # but since get_headers doesn't accept $context parameter, + # we have to change the defaults. + $default = stream_context_get_options(stream_context_get_default()); + stream_context_get_default([ + 'http' => [ + 'method' => 'HEAD', + 'header' => 'Accept: application/xrds+xml, */*', + 'ignore_errors' => true, + ] + ]); + + $url = $url . ($params ? '?' . $params : ''); + $headers_tmp = get_headers($url); + if (!$headers_tmp) { + return []; + } + + # Parsing headers. + $headers = []; + foreach ($headers_tmp as $header) { + $pos = strpos($header, ':'); + $name = strtolower(trim(substr($header, 0, $pos))); + $headers[$name] = trim(substr($header, $pos+1)); + + # Following possible redirections. The point is just to have + # claimed_id change with them, because get_headers() will + # follow redirections automatically. + # We ignore redirections with relative paths. + # If any known provider uses them, file a bug report. + if ($name == 'location') { + if (strpos($headers[$name], 'http') === 0) { + $this->identity = $this->claimed_id = $headers[$name]; + } elseif($headers[$name][0] == '/') { + $parsed_url = parse_url($this->claimed_id); + $this->identity = + $this->claimed_id = $parsed_url['scheme'] . '://' + . $parsed_url['host'] + . $headers[$name]; + } + } + } + + # And restore them. + stream_context_get_default($default); + return $headers; + default: + throw new NotSupportedException("Method {$method} not supported"); + } + + if ($this->verify_peer) { + $options = array_merge( + $options, + [ + 'ssl' => [ + 'verify_peer' => true, + 'capath' => $this->capath, + 'cafile' => $this->cainfo, + ] + ] + ); + } + + $context = stream_context_create($options); + return file_get_contents($url, false, $context); + } + + protected function sendRequest($url, $method = 'GET', $params = []) + { + if (function_exists('curl_init') && !ini_get('safe_mode')) { + return $this->sendCurlRequest($url, $method, $params); + } + return $this->sendStreamRequest($url, $method, $params); + } + + protected function buildUrl($url, $parts) + { + if (isset($url['query'], $parts['query'])) { + $parts['query'] = $url['query'] . '&' . $parts['query']; + } + + $url = $parts + $url; + $url = $url['scheme'] . '://' + . (empty($url['username']) ? '' + :(empty($url['password']) ? "{$url['username']}@" + :"{$url['username']}:{$url['password']}@")) + . $url['host'] + . (empty($url['port']) ? '' : ":{$url['port']}") + . (empty($url['path']) ? '' : $url['path']) + . (empty($url['query']) ? '' : "?{$url['query']}") + . (empty($url['fragment']) ? '' : "#{$url['fragment']}"); + return $url; + } + + /** + * Helper function used to scan for / tags and extract information + * from them + */ + protected function extractHtmlTagValue($content, $tag, $attrName, $attrValue, $valueName) + { + preg_match_all("#<{$tag}[^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*$valueName=['\"](.+?)['\"][^>]*/?>#i", $content, $matches1); + preg_match_all("#<{$tag}[^>]*$valueName=['\"](.+?)['\"][^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*/?>#i", $content, $matches2); + + $result = array_merge($matches1[1], $matches2[1]); + return empty($result) ? false : $result[0]; + } + + /** + * Performs Yadis and HTML discovery. Normally not used. + * @param string $url Identity URL. + * @return string OP Endpoint (i.e. OpenID provider address). + * @throws Exception + */ + public function discover($url) + { + if (!$url) { + throw new Exception('No identity supplied.'); + } + # Use xri.net proxy to resolve i-name identities + if (!preg_match('#^https?:#', $url)) { + $url = "https://xri.net/$url"; + } + + # We save the original url in case of Yadis discovery failure. + # It can happen when we'll be lead to an XRDS document + # which does not have any OpenID2 services. + $originalUrl = $url; + + # A flag to disable yadis discovery in case of failure in headers. + $yadis = true; + + # We'll jump a maximum of 5 times, to avoid endless redirections. + for ($i = 0; $i < 5; $i ++) { + if ($yadis) { + $headers = $this->sendRequest($url, 'HEAD'); + + $next = false; + if (isset($headers['x-xrds-location'])) { + $url = $this->buildUrl(parse_url($url), parse_url(trim($headers['x-xrds-location']))); + $next = true; + } + + if (isset($headers['content-type']) + && (strpos($headers['content-type'], 'application/xrds+xml') !== false + || strpos($headers['content-type'], 'text/xml') !== false) + ) { + # Apparently, some providers return XRDS documents as text/html. + # While it is against the spec, allowing this here shouldn't break + # compatibility with anything. + # --- + # Found an XRDS document, now let's find the server, and optionally delegate. + $content = $this->sendRequest($url, 'GET'); + + preg_match_all('#(.*?)#s', $content, $m); + foreach ($m[1] as $content) { + $content = ' ' . $content; # The space is added, so that strpos doesn't return 0. + + # OpenID 2 + $ns = preg_quote('http://specs.openid.net/auth/2.0/'); + if (preg_match('#\s*'.$ns.'(server|signon)\s*#s', $content, $type)) { + if ($type[1] == 'server') { + $this->identifier_select = true; + } + + preg_match('#(.*)#', $content, $server); + preg_match('#<(Local|Canonical)ID>(.*)#', $content, $delegate); + if (empty($server)) { + return false; + } + # Does the server advertise support for either AX or SREG? + $this->ax = (bool) strpos($content, 'http://openid.net/srv/ax/1.0'); + $this->sreg = strpos($content, 'http://openid.net/sreg/1.0') + || strpos($content, 'http://openid.net/extensions/sreg/1.1'); + + $server = $server[1]; + if (isset($delegate[2])) { + $this->identity = trim($delegate[2]); + } + $this->version = 2; + + $this->server = $server; + return $server; + } + + # OpenID 1.1 + $ns = preg_quote('http://openid.net/signon/1.1'); + if (preg_match('#\s*'.$ns.'\s*#s', $content)) { + + preg_match('#(.*)#', $content, $server); + preg_match('#<.*?Delegate>(.*)#', $content, $delegate); + if (empty($server)) { + return false; + } + # AX can be used only with OpenID 2.0, so checking only SREG + $this->sreg = strpos($content, 'http://openid.net/sreg/1.0') + || strpos($content, 'http://openid.net/extensions/sreg/1.1'); + + $server = $server[1]; + if (isset($delegate[1])) { + $this->identity = $delegate[1]; + } + $this->version = 1; + + $this->server = $server; + return $server; + } + } + + $next = true; + $yadis = false; + $url = $originalUrl; + $content = null; + break; + } + if ($next) { + continue; + } + + # There are no relevant information in headers, so we search the body. + $content = $this->sendRequest($url, 'GET'); + $location = $this->extractHtmlTagValue($content, 'meta', 'http-equiv', 'X-XRDS-Location', 'content'); + if ($location) { + $url = $this->buildUrl(parse_url($url), parse_url($location)); + continue; + } + } + + if (!isset($content)) { + $content = $this->sendRequest($url, 'GET'); + } + + # At this point, the YADIS Discovery has failed, so we'll switch + # to openid2 HTML discovery, then fallback to openid 1.1 discovery. + $server = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid2.provider', 'href'); + $delegate = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid2.local_id', 'href'); + $this->version = 2; + + if (!$server) { + # The same with openid 1.1 + $server = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid.server', 'href'); + $delegate = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid.delegate', 'href'); + $this->version = 1; + } + + if ($server) { + # We found an OpenID2 OP Endpoint + if ($delegate) { + # We have also found an OP-Local ID. + $this->identity = $delegate; + } + $this->server = $server; + return $server; + } + throw new Exception('No servers found!'); + } + throw new Exception('Endless redirection!'); + } + + protected function sregParams() + { + $params = []; + # We always use SREG 1.1, even if the server is advertising only support for 1.0. + # That's because it's fully backwards compatibile with 1.0, and some providers + # advertise 1.0 even if they accept only 1.1. One such provider is myopenid.com + $params['openid.ns.sreg'] = 'http://openid.net/extensions/sreg/1.1'; + if ($this->required) { + $params['openid.sreg.required'] = []; + foreach ($this->required as $required) { + if (!isset(self::$axToSregMap[$required])) { + continue; + } + $params['openid.sreg.required'][] = self::$axToSregMap[$required]; + } + $params['openid.sreg.required'] = implode(',', $params['openid.sreg.required']); + } + + if ($this->optional) { + $params['openid.sreg.optional'] = []; + foreach ($this->optional as $optional) { + if (!isset(self::$axToSregMap[$optional])) { + continue; + } + $params['openid.sreg.optional'][] = self::$axToSregMap[$optional]; + } + $params['openid.sreg.optional'] = implode(',', $params['openid.sreg.optional']); + } + return $params; + } + + protected function axParams() + { + $params = []; + if ($this->required || $this->optional) { + $params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0'; + $params['openid.ax.mode'] = 'fetch_request'; + $this->aliases = []; + $counts = []; + $required = []; + $optional = []; + foreach (['required', 'optional'] as $type) { + foreach ($this->$type as $alias => $field) { + if (is_int($alias)) { + $alias = strtr($field, '/', '_'); + } + $this->aliases[$alias] = 'http://axschema.org/' . $field; + if (empty($counts[$alias])) { + $counts[$alias] = 0; + } + $counts[$alias] += 1; + ${$type}[] = $alias; + } + } + foreach ($this->aliases as $alias => $ns) { + $params['openid.ax.type.' . $alias] = $ns; + } + foreach ($counts as $alias => $count) { + if ($count == 1) { + continue; + } + $params['openid.ax.count.' . $alias] = $count; + } + + # Don't send empty ax.requied and ax.if_available. + # Google and possibly other providers refuse to support ax when one of these is empty. + if ($required) { + $params['openid.ax.required'] = implode(',', $required); + } + if ($optional) { + $params['openid.ax.if_available'] = implode(',', $optional); + } + } + return $params; + } + + protected function authUrlV1() + { + $returnUrl = $this->returnUrl; + # If we have an openid.delegate that is different from our claimed id, + # we need to somehow preserve the claimed id between requests. + # The simplest way is to just send it along with the return_to url. + if ($this->identity != $this->claimed_id) { + $returnUrl .= (strpos($returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $this->claimed_id; + } + + $params = array_merge( + $this->sregParams(), + [ + 'openid.return_to' => $returnUrl, + 'openid.mode' => 'checkid_setup', + 'openid.identity' => $this->identity, + 'openid.trust_root' => $this->trustRoot, + ] + ); + + return $this->buildUrl(parse_url($this->server), ['query' => http_build_query($params, '', '&')]); + } + + protected function authUrlV2($identifierSelect) + { + $params = [ + 'openid.ns' => 'http://specs.openid.net/auth/2.0', + 'openid.mode' => 'checkid_setup', + 'openid.return_to' => $this->returnUrl, + 'openid.realm' => $this->trustRoot, + ]; + if ($this->ax) { + $params = array_merge($this->axParams(), $params); + } + if ($this->sreg) { + $params = array_merge($this->sregParams(), $params); + } + if (!$this->ax && !$this->sreg) { + # If OP doesn't advertise either SREG, nor AX, let's send them both + # in worst case we don't get anything in return. + $params = array_merge($this->sregParams(), $this->axParams(), $params); + } + + if ($identifierSelect) { + $url = 'http://specs.openid.net/auth/2.0/identifier_select'; + $params['openid.identity'] = $url; + $params['openid.claimed_id']= $url; + } else { + $params['openid.identity'] = $this->identity; + $params['openid.claimed_id'] = $this->claimed_id; + } + + return $this->buildUrl(parse_url($this->server), ['query' => http_build_query($params, '', '&')]); + } + + /** + * Returns authentication URL. Usually, you want to redirect your user to it. + * @param string $identifier_select Whether to request OP to select identity for an user in OpenID 2. Does not affect OpenID 1. + * @return string the authentication URL. + * @throws Exception + */ + public function authUrl($identifier_select = null) + { + if (!$this->server) { + $this->discover($this->identity); + } + if ($this->version == 2) { + if ($identifier_select === null) { + return $this->authUrlV2($this->identifier_select); + } + return $this->authUrlV2($identifier_select); + } + return $this->authUrlV1(); + } + + /** + * Performs OpenID verification with the OP. + * @return boolean whether the verification was successful. + * @throws Exception + */ + public function validate() + { + $this->claimed_id = isset($this->data['openid_claimed_id']) ? $this->data['openid_claimed_id'] : $this->data['openid_identity']; + $params = [ + 'openid.assoc_handle' => $this->data['openid_assoc_handle'], + 'openid.signed' => $this->data['openid_signed'], + 'openid.sig' => $this->data['openid_sig'], + ]; + + if (isset($this->data['openid_ns'])) { + # We're dealing with an OpenID 2.0 server, so let's set an ns + # Even though we should know location of the endpoint, + # we still need to verify it by discovery, so $server is not set here + $params['openid.ns'] = 'http://specs.openid.net/auth/2.0'; + } elseif (isset($this->data['openid_claimed_id']) + && $this->data['openid_claimed_id'] != $this->data['openid_identity'] + ) { + # If it's an OpenID 1 provider, and we've got claimed_id, + # we have to append it to the returnUrl, like authUrl_v1 does. + $this->returnUrl .= (strpos($this->returnUrl, '?') ? '&' : '?') + . 'openid.claimed_id=' . $this->claimed_id; + } + + if ($this->data['openid_return_to'] != $this->returnUrl) { + # The return_to url must match the url of current request. + # I'm assuing that noone will set the returnUrl to something that doesn't make sense. + return false; + } + + $server = $this->discover($this->claimed_id); + + foreach (explode(',', $this->data['openid_signed']) as $item) { + # Checking whether magic_quotes_gpc is turned on, because + # the function may fail if it is. For example, when fetching + # AX namePerson, it might containg an apostrophe, which will be escaped. + # In such case, validation would fail, since we'd send different data than OP + # wants to verify. stripslashes() should solve that problem, but we can't + # use it when magic_quotes is off. + $value = $this->data['openid_' . str_replace('.', '_', $item)]; + $params['openid.' . $item] = get_magic_quotes_gpc() ? stripslashes($value) : $value; + } + + $params['openid.mode'] = 'check_authentication'; + + $response = $this->sendRequest($server, 'POST', $params); + + return preg_match('/is_valid\s*:\s*true/i', $response); + } + + protected function getAxAttributes() + { + $alias = null; + if (isset($this->data['openid_ns_ax']) && $this->data['openid_ns_ax'] != 'http://openid.net/srv/ax/1.0') { + # It's the most likely case, so we'll check it before + $alias = 'ax'; + } else { + # 'ax' prefix is either undefined, or points to another extension, + # so we search for another prefix + foreach ($this->data as $key => $value) { + if (substr($key, 0, strlen('openid_ns_')) == 'openid_ns_' && $value == 'http://openid.net/srv/ax/1.0') { + $alias = substr($key, strlen('openid_ns_')); + break; + } + } + } + if (!$alias) { + # An alias for AX schema has not been found, + # so there is no AX data in the OP's response + return []; + } + + $attributes = []; + foreach ($this->data as $key => $value) { + $keyMatch = 'openid_' . $alias . '_value_'; + if (substr($key, 0, strlen($keyMatch)) != $keyMatch) { + continue; + } + $key = substr($key, strlen($keyMatch)); + if (!isset($this->data['openid_' . $alias . '_type_' . $key])) { + # OP is breaking the spec by returning a field without + # associated ns. This shouldn't happen, but it's better + # to check, than cause an E_NOTICE. + continue; + } + $key = substr($this->data['openid_' . $alias . '_type_' . $key], strlen('http://axschema.org/')); + $attributes[$key] = $value; + } + return $attributes; + } + + protected function getSregAttributes() + { + $attributes = array(); + $sregToAx = array_flip(self::$axToSregMap); + foreach ($this->data as $key => $value) { + $keyMatch = 'openid_sreg_'; + if (substr($key, 0, strlen($keyMatch)) != $keyMatch) { + continue; + } + $key = substr($key, strlen($keyMatch)); + if (!isset($sregToAx[$key])) { + # The field name isn't part of the SREG spec, so we ignore it. + continue; + } + $attributes[$sregToAx[$key]] = $value; + } + return $attributes; + } + + /** + * Gets AX/SREG attributes provided by OP. should be used only after successful validaton. + * Note that it does not guarantee that any of the required/optional parameters will be present, + * or that there will be no other attributes besides those specified. + * In other words. OP may provide whatever information it wants to. + * SREG names will be mapped to AX names. + * @return array array of attributes with keys being the AX schema names, e.g. 'contact/email' + * @see http://www.axschema.org/types/ + */ + public function getAttributes() + { + if (isset($this->data['openid_ns']) && $this->data['openid_ns'] == 'http://specs.openid.net/auth/2.0') { + # OpenID 2.0 + # We search for both AX and SREG attributes, with AX taking precedence. + return array_merge($this->getSregAttributes(), $this->getAxAttributes()); + } + return $this->getSregAttributes(); + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/oauth/BaseClient.php b/extensions/yii/authclient/oauth/BaseClient.php deleted file mode 100644 index 1be63cc..0000000 --- a/extensions/yii/authclient/oauth/BaseClient.php +++ /dev/null @@ -1,504 +0,0 @@ - - * @since 2.0 - */ -abstract class BaseClient extends Component -{ - const CONTENT_TYPE_JSON = 'json'; // JSON format - const CONTENT_TYPE_URLENCODED = 'urlencoded'; // urlencoded query string, like name1=value1&name2=value2 - const CONTENT_TYPE_XML = 'xml'; // XML format - const CONTENT_TYPE_AUTO = 'auto'; // attempts to determine format automatically - - /** - * @var string protocol version. - */ - public $version = '1.0'; - /** - * @var string URL, which user will be redirected after authentication at the OAuth provider web site. - * Note: this should be absolute URL (with http:// or https:// leading). - * By default current URL will be used. - */ - private $_returnUrl = ''; - /** - * @var string API base URL. - */ - public $apiBaseUrl = ''; - /** - * @var string authorize URL. - */ - public $authUrl = ''; - /** - * @var string auth request scope. - */ - public $scope = ''; - /** - * @var array cURL request options. Option values from this field will overwrite corresponding - * values from {@link defaultCurlOptions()}. - */ - private $_curlOptions = []; - /** - * @var Token|array access token instance or its array configuration. - */ - private $_accessToken = null; - /** - * @var signature\BaseMethod|array signature method instance or its array configuration. - */ - private $_signatureMethod = []; - - /** - * @param string $returnUrl return URL - */ - public function setReturnUrl($returnUrl) - { - $this->_returnUrl = $returnUrl; - } - - /** - * @return string return URL. - */ - public function getReturnUrl() - { - if (empty($this->_returnUrl)) { - $this->_returnUrl = $this->defaultReturnUrl(); - } - return $this->_returnUrl; - } - - /** - * @param array $curlOptions cURL options. - */ - public function setCurlOptions(array $curlOptions) - { - $this->_curlOptions = $curlOptions; - } - - /** - * @return array cURL options. - */ - public function getCurlOptions() - { - return $this->_curlOptions; - } - - /** - * @param array|Token $token - */ - public function setAccessToken($token) - { - if (!is_object($token)) { - $token = $this->createToken($token); - } - $this->_accessToken = $token; - $this->saveAccessToken($token); - } - - /** - * @return Token auth token instance. - */ - public function getAccessToken() - { - if (!is_object($this->_accessToken)) { - $this->_accessToken = $this->restoreAccessToken(); - } - return $this->_accessToken; - } - - /** - * @param array|signature\BaseMethod $signatureMethod signature method instance or its array configuration. - * @throws InvalidParamException on wrong argument. - */ - public function setSignatureMethod($signatureMethod) - { - if (!is_object($signatureMethod) && !is_array($signatureMethod)) { - throw new InvalidParamException('"'.get_class($this).'::signatureMethod" should be instance of "\yii\autclient\oauth\signature\BaseMethod" or its array configuration. "' . gettype($signatureMethod) . '" has been given.'); - } - $this->_signatureMethod = $signatureMethod; - } - - /** - * @return signature\BaseMethod signature method instance. - */ - public function getSignatureMethod() - { - if (!is_object($this->_signatureMethod)) { - $this->_signatureMethod = $this->createSignatureMethod($this->_signatureMethod); - } - return $this->_signatureMethod; - } - - /** - * Composes default {@link returnUrl} value. - * @return string return URL. - */ - protected function defaultReturnUrl() - { - return Yii::$app->getRequest()->getAbsoluteUrl(); - } - - /** - * Sends HTTP request. - * @param string $method request type. - * @param string $url request URL. - * @param array $params request params. - * @return array response. - * @throws Exception on failure. - */ - protected function sendRequest($method, $url, array $params = []) - { - $curlOptions = $this->mergeCurlOptions( - $this->defaultCurlOptions(), - $this->getCurlOptions(), - array( - CURLOPT_RETURNTRANSFER => true, - CURLOPT_URL => $url, - ), - $this->composeRequestCurlOptions(strtoupper($method), $url, $params) - ); - $curlResource = curl_init(); - foreach ($curlOptions as $option => $value) { - curl_setopt($curlResource, $option, $value); - } - $response = curl_exec($curlResource); - $responseHeaders = curl_getinfo($curlResource); - - // check cURL error - $errorNumber = curl_errno($curlResource); - $errorMessage = curl_error($curlResource); - - curl_close($curlResource); - - if ($errorNumber > 0) { - throw new Exception('Curl error requesting "' . $url . '": #' . $errorNumber . ' - ' . $errorMessage); - } - if ($responseHeaders['http_code'] != 200) { - throw new Exception('Request failed with code: ' . $responseHeaders['http_code'] . ', message: ' . $response); - } - return $this->processResponse($response, $this->determineContentTypeByHeaders($responseHeaders)); - } - - /** - * Merge CUrl options. - * If each options array has an element with the same key value, the latter - * will overwrite the former. - * @param array $options1 options to be merged to. - * @param array $options2 options to be merged from. You can specify additional - * arrays via third argument, fourth argument etc. - * @return array merged options (the original options are not changed.) - */ - protected function mergeCurlOptions($options1, $options2) - { - $args = func_get_args(); - $res = array_shift($args); - while (!empty($args)) { - $next = array_shift($args); - foreach ($next as $k => $v) { - $res[$k]=$v; - } - } - return $res; - } - - /** - * Returns default cURL options. - * @return array cURL options. - */ - protected function defaultCurlOptions() - { - return [ - CURLOPT_USERAGENT => Yii::$app->name . ' OAuth Client', - CURLOPT_CONNECTTIMEOUT => 30, - CURLOPT_TIMEOUT => 30, - CURLOPT_SSL_VERIFYPEER => false, - ]; - } - - /** - * Processes raw response converting it to actual data. - * @param string $rawResponse raw response. - * @param string $contentType response content type. - * @throws Exception on failure. - * @return array actual response. - */ - protected function processResponse($rawResponse, $contentType = self::CONTENT_TYPE_AUTO) - { - if (empty($rawResponse)) { - return []; - } - switch ($contentType) { - case self::CONTENT_TYPE_AUTO: { - $contentType = $this->determineContentTypeByRaw($rawResponse); - if ($contentType == self::CONTENT_TYPE_AUTO) { - throw new Exception('Unable to determine response content type automatically.'); - } - $response = $this->processResponse($rawResponse, $contentType); - break; - } - case self::CONTENT_TYPE_JSON: { - $response = Json::decode($rawResponse, true); - if (isset($response['error'])) { - throw new Exception('Response error: ' . $response['error']); - } - break; - } - case self::CONTENT_TYPE_URLENCODED: { - $response = []; - parse_url($rawResponse, $response); - break; - } - case self::CONTENT_TYPE_XML: { - $response = $this->convertXmlToArray($rawResponse); - break; - } - default: { - throw new Exception('Unknown response type "' . $contentType . '".'); - } - } - return $response; - } - - /** - * Converts XML document to array. - * @param string|\SimpleXMLElement $xml xml to process. - * @return array XML array representation. - */ - protected function convertXmlToArray($xml) - { - if (!is_object($xml)) { - $xml = simplexml_load_string($xml); - } - $result = (array)$xml; - foreach ($result as $key => $value) { - if (is_object($value)) { - $result[$key] = $this->convertXmlToArray($value); - } - } - return $result; - } - - /** - * Attempts to determine HTTP request content type by headers. - * @param array $headers request headers. - * @return string content type. - */ - protected function determineContentTypeByHeaders(array $headers) - { - if (isset($headers['content_type'])) { - if (stripos($headers['content_type'], 'json') !== false) { - return self::CONTENT_TYPE_JSON; - } - if (stripos($headers['content_type'], 'urlencoded') !== false) { - return self::CONTENT_TYPE_URLENCODED; - } - if (stripos($headers['content_type'], 'xml') !== false) { - return self::CONTENT_TYPE_XML; - } - } - return self::CONTENT_TYPE_AUTO; - } - - /** - * Attempts to determine the content type from raw content. - * @param string $rawContent raw response content. - * @return string response type. - */ - protected function determineContentTypeByRaw($rawContent) - { - if (preg_match('/^\\{.*\\}$/is', $rawContent)) { - return self::CONTENT_TYPE_JSON; - } - if (preg_match('/^[^=|^&]+=[^=|^&]+(&[^=|^&]+=[^=|^&]+)*$/is', $rawContent)) { - return self::CONTENT_TYPE_URLENCODED; - } - if (preg_match('/^<.*>$/is', $rawContent)) { - return self::CONTENT_TYPE_XML; - } - return self::CONTENT_TYPE_AUTO; - } - - /** - * Creates signature method instance from its configuration. - * @param array $signatureMethodConfig signature method configuration. - * @return signature\BaseMethod signature method instance. - */ - protected function createSignatureMethod(array $signatureMethodConfig) - { - if (!array_key_exists('class', $signatureMethodConfig)) { - $signatureMethodConfig['class'] = signature\HmacSha1::className(); - } - return Yii::createObject($signatureMethodConfig); - } - - /** - * Creates token from its configuration. - * @param array $tokenConfig token configuration. - * @return Token token instance. - */ - protected function createToken(array $tokenConfig = []) - { - if (!array_key_exists('class', $tokenConfig)) { - $tokenConfig['class'] = Token::className(); - } - return Yii::createObject($tokenConfig); - } - - /** - * Composes URL from base URL and GET params. - * @param string $url base URL. - * @param array $params GET params. - * @return string composed URL. - */ - protected function composeUrl($url, array $params = []) - { - if (strpos($url, '?') === false) { - $url .= '?'; - } else { - $url .= '&'; - } - $url .= http_build_query($params, '', '&', PHP_QUERY_RFC3986); - return $url; - } - - /** - * Saves token as persistent state. - * @param Token $token auth token - * @return static self reference. - */ - protected function saveAccessToken(Token $token) - { - return $this->setState('token', $token); - } - - /** - * Restores access token. - * @return Token auth token. - */ - protected function restoreAccessToken() - { - $token = $this->getState('token'); - if (is_object($token)) { - /* @var $token Token */ - if ($token->getIsExpired()) { - $token = $this->refreshAccessToken($token); - } - } - return $token; - } - - /** - * Sets persistent state. - * @param string $key state key. - * @param mixed $value state value - * @return static self reference. - */ - protected function setState($key, $value) - { - $session = Yii::$app->getSession(); - $key = $this->getStateKeyPrefix() . $key; - $session->set($key, $value); - return $this; - } - - /** - * Returns persistent state value. - * @param string $key state key. - * @return mixed state value. - */ - protected function getState($key) - { - $session = Yii::$app->getSession(); - $key = $this->getStateKeyPrefix() . $key; - $value = $session->get($key); - return $value; - } - - /** - * Removes persistent state value. - * @param string $key state key. - * @return boolean success. - */ - protected function removeState($key) - { - $session = Yii::$app->getSession(); - $key = $this->getStateKeyPrefix() . $key; - $session->remove($key); - return true; - } - - /** - * Returns session key prefix, which is used to store internal states. - * @return string session key prefix. - */ - protected function getStateKeyPrefix() - { - return get_class($this) . '_' . sha1($this->authUrl) . '_'; - } - - /** - * Performs request to the OAuth API. - * @param string $apiSubUrl API sub URL, which will be append to [[apiBaseUrl]], or absolute API URL. - * @param string $method request method. - * @param array $params request parameters. - * @return array API response - * @throws Exception on failure. - */ - public function api($apiSubUrl, $method = 'GET', array $params = []) - { - if (preg_match('/^https?:\\/\\//is', $apiSubUrl)) { - $url = $apiSubUrl; - } else { - $url = $this->apiBaseUrl . '/' . $apiSubUrl; - } - $accessToken = $this->getAccessToken(); - if (!is_object($accessToken) || !$accessToken->getIsValid()) { - throw new Exception('Invalid access token.'); - } - return $this->apiInternal($accessToken, $url, $method, $params); - } - - /** - * Composes HTTP request CUrl options, which will be merged with the default ones. - * @param string $method request type. - * @param string $url request URL. - * @param array $params request params. - * @return array CUrl options. - * @throws Exception on failure. - */ - abstract protected function composeRequestCurlOptions($method, $url, array $params); - - /** - * Gets new auth token to replace expired one. - * @param Token $token expired auth token. - * @return Token new auth token. - */ - abstract public function refreshAccessToken(Token $token); - - /** - * Performs request to the OAuth API. - * @param Token $accessToken actual access token. - * @param string $url absolute API URL. - * @param string $method request method. - * @param array $params request parameters. - * @return array API response. - * @throws Exception on failure. - */ - abstract protected function apiInternal($accessToken, $url, $method, array $params); -} \ No newline at end of file diff --git a/extensions/yii/authclient/oauth/Client1.php b/extensions/yii/authclient/oauth/Client1.php deleted file mode 100644 index 5165879..0000000 --- a/extensions/yii/authclient/oauth/Client1.php +++ /dev/null @@ -1,353 +0,0 @@ -fetchRequestToken(); // Get request token - * $url = $oauthClient->buildAuthUrl($requestToken); // Get authorization URL - * Yii::$app->getResponse()->redirect($url); // Redirect to authorization URL - * // After user returns at our site: - * $accessToken = $oauthClient->fetchAccessToken($requestToken); // Upgrade to access token - * ~~~ - * - * @see http://oauth.net/ - * - * @author Paul Klimov - * @since 2.0 - */ -class Client1 extends BaseClient -{ - /** - * @var string protocol version. - */ - public $version = '1.0'; - /** - * @var string OAuth consumer key. - */ - public $consumerKey = ''; - /** - * @var string OAuth consumer secret. - */ - public $consumerSecret = ''; - /** - * @var string OAuth request token URL. - */ - public $requestTokenUrl = ''; - /** - * @var string request token HTTP method. - */ - public $requestTokenMethod = 'GET'; - /** - * @var string OAuth access token URL. - */ - public $accessTokenUrl = ''; - /** - * @var string access token HTTP method. - */ - public $accessTokenMethod = 'GET'; - - /** - * Fetches the OAuth request token. - * @param array $params additional request params. - * @return Token request token. - */ - public function fetchRequestToken(array $params = []) - { - $this->removeState('token'); - $defaultParams = [ - 'oauth_consumer_key' => $this->consumerKey, - 'oauth_callback' => $this->getReturnUrl(), - //'xoauth_displayname' => Yii::$app->name, - ]; - if (!empty($this->scope)) { - $defaultParams['scope'] = $this->scope; - } - $response = $this->sendSignedRequest($this->requestTokenMethod, $this->requestTokenUrl, array_merge($defaultParams, $params)); - $token = $this->createToken([ - 'params' => $response - ]); - $this->setState('requestToken', $token); - return $token; - } - - /** - * Composes user authorization URL. - * @param Token $requestToken OAuth request token. - * @param array $params additional request params. - * @return string authorize URL - * @throws Exception on failure. - */ - public function buildAuthUrl(Token $requestToken = null, array $params = []) - { - if (!is_object($requestToken)) { - $requestToken = $this->getState('requestToken'); - if (!is_object($requestToken)) { - throw new Exception('Request token is required to build authorize URL!'); - } - } - $params['oauth_token'] = $requestToken->getToken(); - return $this->composeUrl($this->authUrl, $params); - } - - /** - * Fetches OAuth access token. - * @param Token $requestToken OAuth request token. - * @param string $oauthVerifier OAuth verifier. - * @param array $params additional request params. - * @return Token OAuth access token. - * @throws Exception on failure. - */ - public function fetchAccessToken(Token $requestToken = null, $oauthVerifier = null, array $params = []) - { - if (!is_object($requestToken)) { - $requestToken = $this->getState('requestToken'); - if (!is_object($requestToken)) { - throw new Exception('Request token is required to fetch access token!'); - } - } - $this->removeState('requestToken'); - $defaultParams = [ - 'oauth_consumer_key' => $this->consumerKey, - 'oauth_token' => $requestToken->getToken() - ]; - if ($oauthVerifier === null) { - if (isset($_REQUEST['oauth_verifier'])) { - $oauthVerifier = $_REQUEST['oauth_verifier']; - } - } - if (!empty($oauthVerifier)) { - $defaultParams['oauth_verifier'] = $oauthVerifier; - } - $response = $this->sendSignedRequest($this->accessTokenMethod, $this->accessTokenUrl, array_merge($defaultParams, $params)); - - $token = $this->createToken([ - 'params' => $response - ]); - $this->setAccessToken($token); - return $token; - } - - /** - * Sends HTTP request, signed by {@link signatureMethod}. - * @param string $method request type. - * @param string $url request URL. - * @param array $params request params. - * @return array response. - */ - protected function sendSignedRequest($method, $url, array $params = []) - { - $params = array_merge($params, $this->generateCommonRequestParams()); - $params = $this->signRequest($method, $url, $params); - return $this->sendRequest($method, $url, $params); - } - - /** - * Composes HTTP request CUrl options, which will be merged with the default ones. - * @param string $method request type. - * @param string $url request URL. - * @param array $params request params. - * @return array CUrl options. - * @throws Exception on failure. - */ - protected function composeRequestCurlOptions($method, $url, array $params) - { - $curlOptions = []; - switch ($method) { - case 'GET': { - $curlOptions[CURLOPT_URL] = $this->composeUrl($url, $params); - break; - } - case 'POST': { - $curlOptions[CURLOPT_POST] = true; - if (!empty($params)){ - $curlOptions[CURLOPT_POSTFIELDS] = $params; - } - $authorizationHeader = $this->composeAuthorizationHeader($params); - if (!empty($authorizationHeader)/* && $this->curlAuthHeader*/) { - $curlOptions[CURLOPT_HTTPHEADER] = ['Content-Type: application/atom+xml', $authorizationHeader]; - } - break; - } - case 'HEAD': - case 'PUT': - case 'DELETE': { - $curlOptions[CURLOPT_CUSTOMREQUEST] = $method; - if (!empty($params)) { - $curlOptions[CURLOPT_URL] = $this->composeUrl($url, $params); - } - break; - } - default: { - throw new Exception("Unknown request method '{$method}'."); - } - } - return $curlOptions; - } - - /** - * Performs request to the OAuth API. - * @param Token $accessToken actual access token. - * @param string $url absolute API URL. - * @param string $method request method. - * @param array $params request parameters. - * @return array API response. - * @throws Exception on failure. - */ - protected function apiInternal($accessToken, $url, $method, array $params) - { - $params['oauth_consumer_key'] = $this->consumerKey; - $params['oauth_token'] = $accessToken->getToken(); - $response = $this->sendSignedRequest($method, $url, $params); - return $response; - } - - /** - * Gets new auth token to replace expired one. - * @param Token $token expired auth token. - * @return Token new auth token. - */ - public function refreshAccessToken(Token $token) - { - // @todo - return null; - } - - /** - * Composes default {@link returnUrl} value. - * @return string return URL. - */ - protected function defaultReturnUrl() - { - $params = $_GET; - unset($params['oauth_token']); - return Yii::$app->getUrlManager()->createAbsoluteUrl(Yii::$app->controller->getRoute(), $params); - } - - /** - * Generates nonce value. - * @return string nonce value. - */ - protected function generateNonce() - { - return md5(microtime() . mt_rand()); - } - - /** - * Generates timestamp. - * @return integer timestamp. - */ - protected function generateTimestamp() - { - return time(); - } - - /** - * Generate common request params like version, timestamp etc. - * @return array common request params. - */ - protected function generateCommonRequestParams() - { - $params = [ - 'oauth_version' => $this->version, - 'oauth_nonce' => $this->generateNonce(), - 'oauth_timestamp' => $this->generateTimestamp(), - ]; - return $params; - } - - /** - * Sign request with {@link signatureMethod}. - * @param string $method request method. - * @param string $url request URL. - * @param array $params request params. - * @return array signed request params. - */ - protected function signRequest($method, $url, array $params) - { - $signatureMethod = $this->getSignatureMethod(); - $params['oauth_signature_method'] = $signatureMethod->getName(); - $signatureBaseString = $this->composeSignatureBaseString($method, $url, $params); - $signatureKey = $this->composeSignatureKey(); - $params['oauth_signature'] = $signatureMethod->generateSignature($signatureBaseString, $signatureKey); - return $params; - } - - /** - * Creates signature base string, which will be signed by {@link signatureMethod}. - * @param string $method request method. - * @param string $url request URL. - * @param array $params request params. - * @return string base signature string. - */ - protected function composeSignatureBaseString($method, $url, array $params) - { - unset($params['oauth_signature']); - $parts = [ - strtoupper($method), - $url, - http_build_query($params, '', '&', PHP_QUERY_RFC3986) - ]; - $parts = array_map('rawurlencode', $parts); - return implode('&', $parts); - } - - /** - * Composes request signature key. - * @return string signature key. - */ - protected function composeSignatureKey() - { - $signatureKeyParts = [ - $this->consumerSecret - ]; - $accessToken = $this->getAccessToken(); - if (is_object($accessToken)) { - $signatureKeyParts[] = $accessToken->getTokenSecret(); - } else { - $signatureKeyParts[] = ''; - } - $signatureKeyParts = array_map('rawurlencode', $signatureKeyParts); - return implode('&', $signatureKeyParts); - } - - /** - * Composes authorization header content. - * @param array $params request params. - * @param string $realm authorization realm. - * @return string authorization header content. - */ - protected function composeAuthorizationHeader(array $params, $realm = '') - { - $header = 'Authorization: OAuth'; - $headerParams = []; - if (!empty($realm)) { - $headerParams[] = 'realm="' . rawurlencode($realm) . '"'; - } - foreach ($params as $key => $value) { - if (substr($key, 0, 5) != 'oauth') { - continue; - } - $headerParams[] = rawurlencode($key) . '="' . rawurlencode($value) . '"'; - } - if (!empty($headerParams)) { - $header .= ' ' . implode(', ', $headerParams); - } - return $header; - } -} \ No newline at end of file diff --git a/extensions/yii/authclient/oauth/Client2.php b/extensions/yii/authclient/oauth/Client2.php deleted file mode 100644 index 128fad5..0000000 --- a/extensions/yii/authclient/oauth/Client2.php +++ /dev/null @@ -1,184 +0,0 @@ -buildAuthUrl(); // Build authorization URL - * Yii::$app->getResponse()->redirect($url); // Redirect to authorization URL. - * // After user returns at our site: - * $code = $_GET['code']; - * $accessToken = $oauthClient->fetchAccessToken($code); // Get access token - * ~~~ - * - * @see http://oauth.net/2/ - * - * @author Paul Klimov - * @since 2.0 - */ -class Client2 extends BaseClient -{ - /** - * @var string protocol version. - */ - public $version = '2.0'; - /** - * @var string OAuth client ID. - */ - public $clientId = ''; - /** - * @var string OAuth client secret. - */ - public $clientSecret = ''; - /** - * @var string token request URL endpoint. - */ - public $tokenUrl = ''; - - /** - * Composes user authorization URL. - * @param array $params additional auth GET params. - * @return string authorization URL. - */ - public function buildAuthUrl(array $params = []) - { - $defaultParams = [ - 'client_id' => $this->clientId, - 'response_type' => 'code', - 'redirect_uri' => $this->getReturnUrl(), - 'xoauth_displayname' => Yii::$app->name, - ]; - if (!empty($this->scope)) { - $defaultParams['scope'] = $this->scope; - } - return $this->composeUrl($this->authUrl, array_merge($defaultParams, $params)); - } - - /** - * Fetches access token from authorization code. - * @param string $authCode authorization code, usually comes at $_GET['code']. - * @param array $params additional request params. - * @return Token access token. - */ - public function fetchAccessToken($authCode, array $params = []) - { - $defaultParams = [ - 'client_id' => $this->clientId, - 'client_secret' => $this->clientSecret, - 'code' => $authCode, - 'grant_type' => 'authorization_code', - 'redirect_uri' => $this->getReturnUrl(), - ]; - $response = $this->sendRequest('POST', $this->tokenUrl, array_merge($defaultParams, $params)); - $token = $this->createToken(['params' => $response]); - $this->setAccessToken($token); - return $token; - } - - /** - * Composes HTTP request CUrl options, which will be merged with the default ones. - * @param string $method request type. - * @param string $url request URL. - * @param array $params request params. - * @return array CUrl options. - * @throws Exception on failure. - */ - protected function composeRequestCurlOptions($method, $url, array $params) - { - $curlOptions = []; - switch ($method) { - case 'GET': { - $curlOptions[CURLOPT_URL] = $this->composeUrl($url, $params); - break; - } - case 'POST': { - $curlOptions[CURLOPT_POST] = true; - $curlOptions[CURLOPT_HTTPHEADER] = ['Content-type: application/x-www-form-urlencoded']; - $curlOptions[CURLOPT_POSTFIELDS] = http_build_query($params, '', '&', PHP_QUERY_RFC3986); - break; - } - case 'HEAD': - case 'PUT': - case 'DELETE': { - $curlOptions[CURLOPT_CUSTOMREQUEST] = $method; - if (!empty($params)) { - $curlOptions[CURLOPT_URL] = $this->composeUrl($url, $params); - } - break; - } - default: { - throw new Exception("Unknown request method '{$method}'."); - } - } - return $curlOptions; - } - - /** - * Performs request to the OAuth API. - * @param Token $accessToken actual access token. - * @param string $url absolute API URL. - * @param string $method request method. - * @param array $params request parameters. - * @return array API response. - * @throws Exception on failure. - */ - protected function apiInternal($accessToken, $url, $method, array $params) - { - $params['access_token'] = $accessToken->getToken(); - return $this->sendRequest($method, $url, $params); - } - - /** - * Gets new auth token to replace expired one. - * @param Token $token expired auth token. - * @return Token new auth token. - */ - public function refreshAccessToken(Token $token) - { - $params = [ - 'client_id' => $this->clientId, - 'client_secret' => $this->clientSecret, - 'grant_type' => 'refresh_token' - ]; - $params = array_merge($token->getParams(), $params); - $response = $this->sendRequest('POST', $this->tokenUrl, $params); - return $response; - } - - /** - * Composes default {@link returnUrl} value. - * @return string return URL. - */ - protected function defaultReturnUrl() - { - $params = $_GET; - unset($params['code']); - return Yii::$app->getUrlManager()->createAbsoluteUrl(Yii::$app->controller->getRoute(), $params); - } - - /** - * Creates token from its configuration. - * @param array $tokenConfig token configuration. - * @return Token token instance. - */ - protected function createToken(array $tokenConfig = []) - { - $tokenConfig['tokenParamKey'] = 'access_token'; - return parent::createToken($tokenConfig); - } -} \ No newline at end of file diff --git a/extensions/yii/authclient/oauth/Token.php b/extensions/yii/authclient/oauth/Token.php deleted file mode 100644 index 16e6d1e..0000000 --- a/extensions/yii/authclient/oauth/Token.php +++ /dev/null @@ -1,186 +0,0 @@ - - * @since 2.0 - */ -class Token extends Object -{ - /** - * @var string key in {@link _params} array, which stores token key. - */ - public $tokenParamKey = 'oauth_token'; - /** - * @var string key in {@link _params} array, which stores token secret key. - */ - public $tokenSecretParamKey = 'oauth_token_secret'; - /** - * @var string key in {@link _params} array, which stores token expiration duration. - * If not set will attempt to fetch its value automatically. - */ - private $_expireDurationParamKey; - /** - * @var array token parameters. - */ - private $_params = []; - /** - * @var integer object creation timestamp. - */ - public $createTimestamp; - - public function init() - { - if ($this->createTimestamp === null) { - $this->createTimestamp = time(); - } - } - - /** - * @param string $expireDurationParamKey expire duration param key. - */ - public function setExpireDurationParamKey($expireDurationParamKey) { - $this->_expireDurationParamKey = $expireDurationParamKey; - } - - /** - * @return string expire duration param key. - */ - public function getExpireDurationParamKey() { - if ($this->_expireDurationParamKey === null) { - $this->_expireDurationParamKey = $this->defaultExpireDurationParamKey(); - } - return $this->_expireDurationParamKey; - } - - /** - * @return array - */ - public function getParams() { - return $this->_params; - } - - /** - * @param array $params - */ - public function setParams(array $params) { - $this->_params = $params; - } - - /** - * Sets param by name. - * @param string $name param name. - * @param mixed $value param value, - */ - public function setParam($name, $value) { - $this->_params[$name] = $value; - } - - /** - * Returns param by name. - * @param string $name param name. - * @return mixed param value. - */ - public function getParam($name) { - return isset($this->_params[$name]) ? $this->_params[$name] : null; - } - - /** - * Sets token value. - * @param string $token token value. - * @return static self reference. - */ - public function setToken($token) { - $this->setParam($this->tokenParamKey, $token); - } - - /** - * Returns token value. - * @return string token value. - */ - public function getToken() { - return $this->getParam($this->tokenParamKey); - } - - /** - * Sets the token secret value. - * @param string $tokenSecret token secret. - */ - public function setTokenSecret($tokenSecret) { - $this->setParam($this->tokenSecretParamKey, $tokenSecret); - } - - /** - * Returns the token secret value. - * @return string token secret value. - */ - public function getTokenSecret() { - return $this->getParam($this->tokenSecretParamKey); - } - - /** - * Sets token expire duration. - * @param string $expireDuration token expiration duration. - */ - public function setExpireDuration($expireDuration) { - $this->setParam($this->getExpireDurationParamKey(), $expireDuration); - } - - /** - * Returns the token expiration duration. - * @return integer token expiration duration. - */ - public function getExpireDuration() { - return $this->getParam($this->getExpireDurationParamKey()); - } - - /** - * Fetches default expire duration param key. - * @return string expire duration param key. - */ - protected function defaultExpireDurationParamKey() { - $expireDurationParamKey = 'expires_in'; - foreach ($this->getParams() as $name => $value) { - if (strpos($name, 'expir') !== false) { - $expireDurationParamKey = $name; - break; - } - } - return $expireDurationParamKey; - } - - /** - * Checks if token has expired. - * @return boolean is token expired. - */ - public function getIsExpired() { - $expirationDuration = $this->getExpireDuration(); - if (empty($expirationDuration)) { - return false; - } - return (time() >= ($this->createTimestamp + $expirationDuration)); - } - - /** - * Checks if token is valid. - * @return boolean is token valid. - */ - public function getIsValid() { - $token = $this->getToken(); - return (!empty($token) && !$this->getIsExpired()); - } -} \ No newline at end of file diff --git a/extensions/yii/authclient/oauth/signature/BaseMethod.php b/extensions/yii/authclient/oauth/signature/BaseMethod.php deleted file mode 100644 index 1fcefdf..0000000 --- a/extensions/yii/authclient/oauth/signature/BaseMethod.php +++ /dev/null @@ -1,51 +0,0 @@ - - * @since 2.0 - */ -abstract class BaseMethod extends Object -{ - /** - * Return the canonical name of the Signature Method. - * @return string method name. - */ - abstract public function getName(); - - /** - * Generates OAuth request signature. - * @param string $baseString signature base string. - * @param string $key signature key. - * @return string signature string. - */ - abstract public function generateSignature($baseString, $key); - - /** - * Verifies given OAuth request. - * @param string $signature signature to be verified. - * @param string $baseString signature base string. - * @param string $key signature key. - * @return boolean success. - */ - public function verify($signature, $baseString, $key) - { - $expectedSignature = $this->generateSignature($baseString, $key); - if (empty($signature) || empty($expectedSignature)) { - return false; - } - return (strcmp($expectedSignature, $signature) === 0); - } -} \ No newline at end of file diff --git a/extensions/yii/authclient/oauth/signature/HmacSha1.php b/extensions/yii/authclient/oauth/signature/HmacSha1.php deleted file mode 100644 index 3a7f223..0000000 --- a/extensions/yii/authclient/oauth/signature/HmacSha1.php +++ /dev/null @@ -1,47 +0,0 @@ - - * @since 2.0 - */ -class HmacSha1 extends BaseMethod -{ - /** - * @inheritdoc - */ - public function init() - { - if (!function_exists('hash_hmac')) { - throw new NotSupportedException('PHP "Hash" extension is required.'); - } - } - - /** - * @inheritdoc - */ - public function getName() - { - return 'HMAC-SHA1'; - } - - /** - * @inheritdoc - */ - public function generateSignature($baseString, $key) - { - return base64_encode(hash_hmac('sha1', $baseString, $key, true)); - } -} \ No newline at end of file diff --git a/extensions/yii/authclient/oauth/signature/PlainText.php b/extensions/yii/authclient/oauth/signature/PlainText.php deleted file mode 100644 index 1140814..0000000 --- a/extensions/yii/authclient/oauth/signature/PlainText.php +++ /dev/null @@ -1,33 +0,0 @@ - - * @since 2.0 - */ -class PlainText extends BaseMethod -{ - /** - * @inheritdoc - */ - public function getName() - { - return 'PLAINTEXT'; - } - - /** - * @inheritdoc - */ - public function generateSignature($baseString, $key) - { - return $key; - } -} \ No newline at end of file diff --git a/extensions/yii/authclient/oauth/signature/RsaSha1.php b/extensions/yii/authclient/oauth/signature/RsaSha1.php deleted file mode 100644 index e140fd5..0000000 --- a/extensions/yii/authclient/oauth/signature/RsaSha1.php +++ /dev/null @@ -1,168 +0,0 @@ - - * @since 2.0 - */ -class RsaSha1 extends BaseMethod -{ - /** - * @var string OpenSSL private key certificate content. - * This value can be fetched from file specified by {@link privateCertificateFile}. - */ - protected $_privateCertificate; - /** - * @var string OpenSSL public key certificate content. - * This value can be fetched from file specified by {@link publicCertificateFile}. - */ - protected $_publicCertificate; - /** - * @var string path to the file, which holds private key certificate. - */ - public $privateCertificateFile = ''; - /** - * @var string path to the file, which holds public key certificate. - */ - public $publicCertificateFile = ''; - - /** - * @inheritdoc - */ - public function init() - { - if (!function_exists('openssl_sign')) { - throw new NotSupportedException('PHP "OpenSSL" extension is required.'); - } - } - - /** - * @param string $publicCertificate public key certificate content. - */ - public function setPublicCertificate($publicCertificate) - { - $this->_publicCertificate = $publicCertificate; - } - - /** - * @return string public key certificate content. - */ - public function getPublicCertificate() - { - if ($this->_publicCertificate === null) { - $this->_publicCertificate = $this->initPublicCertificate(); - } - return $this->_publicCertificate; - } - - /** - * @param string $privateCertificate private key certificate content. - */ - public function setPrivateCertificate($privateCertificate) - { - $this->_privateCertificate = $privateCertificate; - } - - /** - * @return string private key certificate content. - */ - public function getPrivateCertificate() - { - if ($this->_privateCertificate === null) { - $this->_privateCertificate = $this->initPrivateCertificate(); - } - return $this->_privateCertificate; - } - - /** - * @inheritdoc - */ - public function getName() - { - return 'RSA-SHA1'; - } - - /** - * Creates initial value for {@link publicCertificate}. - * This method will attempt to fetch the certificate value from {@link publicCertificateFile} file. - * @throws InvalidConfigException on failure. - * @return string public certificate content. - */ - protected function initPublicCertificate() - { - if (!empty($this->publicCertificateFile)) { - if (!file_exists($this->publicCertificateFile)) { - throw new InvalidConfigException("Public certificate file '{$this->publicCertificateFile}' does not exist!"); - } - return file_get_contents($this->publicCertificateFile); - } else { - return ''; - } - } - - /** - * Creates initial value for {@link privateCertificate}. - * This method will attempt to fetch the certificate value from {@link privateCertificateFile} file. - * @throws InvalidConfigException on failure. - * @return string private certificate content. - */ - protected function initPrivateCertificate() - { - if (!empty($this->privateCertificateFile)) { - if (!file_exists($this->privateCertificateFile)) { - throw new InvalidConfigException("Private certificate file '{$this->privateCertificateFile}' does not exist!"); - } - return file_get_contents($this->privateCertificateFile); - } else { - return ''; - } - } - - /** - * @inheritdoc - */ - public function generateSignature($baseString, $key) - { - $privateCertificateContent = $this->getPrivateCertificate(); - // Pull the private key ID from the certificate - $privateKeyId = openssl_pkey_get_private($privateCertificateContent); - // Sign using the key - openssl_sign($baseString, $signature, $privateKeyId); - // Release the key resource - openssl_free_key($privateKeyId); - return base64_encode($signature); - } - - /** - * @inheritdoc - */ - public function verify($signature, $baseString, $key) - { - $decodedSignature = base64_decode($signature); - // Fetch the public key cert based on the request - $publicCertificate = $this->getPublicCertificate(); - // Pull the public key ID from the certificate - $publicKeyId = openssl_pkey_get_public($publicCertificate); - // Check the computed signature against the one passed in the query - $verificationResult = openssl_verify($baseString, $decodedSignature, $publicKeyId); - // Release the key resource - openssl_free_key($publicKeyId); - return ($verificationResult == 1); - } -} \ No newline at end of file diff --git a/extensions/yii/authclient/openid/Client.php b/extensions/yii/authclient/openid/Client.php deleted file mode 100644 index bd06988..0000000 --- a/extensions/yii/authclient/openid/Client.php +++ /dev/null @@ -1,780 +0,0 @@ - - * @since 2.0 - */ -class Client extends Component -{ - public $required = []; - public $optional = []; - public $verify_peer; - public $capath; - public $cainfo; - - private $_returnUrl; - private $_identity; - private $claimed_id; - private $_trustRoot; - - protected $server; - protected $version; - - protected $aliases; - protected $identifier_select = false; - protected $ax = false; - protected $sreg = false; - protected $data; - - public static $axToSregMap = [ - 'namePerson/friendly' => 'nickname', - 'contact/email' => 'email', - 'namePerson' => 'fullname', - 'birthDate' => 'dob', - 'person/gender' => 'gender', - 'contact/postalCode/home' => 'postcode', - 'contact/country/home' => 'country', - 'pref/language' => 'language', - 'pref/timezone' => 'timezone', - ]; - - /** - * @inheritdoc - */ - public function init() - { - $this->data = $_POST + $_GET; # OPs may send data as POST or GET. - } - - public function setIdentity($value) - { - if (strlen($value = trim((String) $value))) { - if (preg_match('#^xri:/*#i', $value, $m)) { - $value = substr($value, strlen($m[0])); - } elseif (!preg_match('/^(?:[=@+\$!\(]|https?:)/i', $value)) { - $value = "http://$value"; - } - if (preg_match('#^https?://[^/]+$#i', $value, $m)) { - $value .= '/'; - } - } - $this->_identity = $value; - $this->claimed_id = $value; - } - - public function setReturnUrl($returnUrl) - { - $this->_returnUrl = $returnUrl; - } - - public function getReturnUrl() - { - if ($this->_returnUrl === null) { - $uri = rtrim(preg_replace('#((?<=\?)|&)openid\.[^&]+#', '', $_SERVER['REQUEST_URI']), '?'); - $this->_returnUrl = $this->getTrustRoot() . $uri; - } - return $this->_returnUrl; - } - - public function getIdentity() - { - # We return claimed_id instead of identity, - # because the developer should see the claimed identifier, - # i.e. what he set as identity, not the op-local identifier (which is what we verify) - return $this->claimed_id; - } - - public function setTrustRoot($value) - { - $this->_trustRoot = trim($value); - } - - public function getTrustRoot() - { - if ($this->_trustRoot === null) { - $this->_trustRoot = (!empty($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST']; - } - return $this->_trustRoot; - } - - public function setRealm($value) - { - $this->setTrustRoot($value); - } - - public function getRealm() - { - return $this->getTrustRoot(); - } - - public function getMode() - { - return empty($this->data['openid_mode']) ? null : $this->data['openid_mode']; - } - - /** - * Checks if the server specified in the url exists. - * @param string $url URL to check - * @return boolean true, if the server exists; false otherwise - */ - public function hostExists($url) - { - if (strpos($url, '/') === false) { - $server = $url; - } else { - $server = @parse_url($url, PHP_URL_HOST); - } - if (!$server) { - return false; - } - $ips = gethostbynamel($server); - return !empty($ips); - } - - protected function sendCurlRequest($url, $method = 'GET', $params = []) - { - $params = http_build_query($params, '', '&'); - $curl = curl_init($url . ($method == 'GET' && $params ? '?' . $params : '')); - curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($curl, CURLOPT_HEADER, false); - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); - curl_setopt($curl, CURLOPT_HTTPHEADER, array('Accept: application/xrds+xml, */*')); - - if ($this->verify_peer !== null) { - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verify_peer); - if($this->capath) { - curl_setopt($curl, CURLOPT_CAPATH, $this->capath); - } - if($this->cainfo) { - curl_setopt($curl, CURLOPT_CAINFO, $this->cainfo); - } - } - - if ($method == 'POST') { - curl_setopt($curl, CURLOPT_POST, true); - curl_setopt($curl, CURLOPT_POSTFIELDS, $params); - } elseif ($method == 'HEAD') { - curl_setopt($curl, CURLOPT_HEADER, true); - curl_setopt($curl, CURLOPT_NOBODY, true); - } else { - curl_setopt($curl, CURLOPT_HTTPGET, true); - } - $response = curl_exec($curl); - - if ($method == 'HEAD') { - $headers = []; - foreach (explode("\n", $response) as $header) { - $pos = strpos($header,':'); - $name = strtolower(trim(substr($header, 0, $pos))); - $headers[$name] = trim(substr($header, $pos+1)); - } - - # Updating claimed_id in case of redirections. - $effective_url = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL); - if ($effective_url != $url) { - $this->identity = $this->claimed_id = $effective_url; - } - - return $headers; - } - - if (curl_errno($curl)) { - throw new Exception(curl_error($curl), curl_errno($curl)); - } - - return $response; - } - - protected function sendStreamRequest($url, $method = 'GET', $params = []) - { - if (!$this->hostExists($url)) { - throw new Exception('Invalid request.'); - } - - $params = http_build_query($params, '', '&'); - switch ($method) { - case 'GET': - $options = [ - 'http' => [ - 'method' => 'GET', - 'header' => 'Accept: application/xrds+xml, */*', - 'ignore_errors' => true, - ] - ]; - $url = $url . ($params ? '?' . $params : ''); - break; - case 'POST': - $options = [ - 'http' => [ - 'method' => 'POST', - 'header' => 'Content-type: application/x-www-form-urlencoded', - 'content' => $params, - 'ignore_errors' => true, - ] - ]; - break; - case 'HEAD': - # We want to send a HEAD request, - # but since get_headers doesn't accept $context parameter, - # we have to change the defaults. - $default = stream_context_get_options(stream_context_get_default()); - stream_context_get_default([ - 'http' => [ - 'method' => 'HEAD', - 'header' => 'Accept: application/xrds+xml, */*', - 'ignore_errors' => true, - ] - ]); - - $url = $url . ($params ? '?' . $params : ''); - $headers_tmp = get_headers($url); - if (!$headers_tmp) { - return []; - } - - # Parsing headers. - $headers = []; - foreach ($headers_tmp as $header) { - $pos = strpos($header, ':'); - $name = strtolower(trim(substr($header, 0, $pos))); - $headers[$name] = trim(substr($header, $pos+1)); - - # Following possible redirections. The point is just to have - # claimed_id change with them, because get_headers() will - # follow redirections automatically. - # We ignore redirections with relative paths. - # If any known provider uses them, file a bug report. - if ($name == 'location') { - if (strpos($headers[$name], 'http') === 0) { - $this->identity = $this->claimed_id = $headers[$name]; - } elseif($headers[$name][0] == '/') { - $parsed_url = parse_url($this->claimed_id); - $this->identity = - $this->claimed_id = $parsed_url['scheme'] . '://' - . $parsed_url['host'] - . $headers[$name]; - } - } - } - - # And restore them. - stream_context_get_default($default); - return $headers; - default: - throw new NotSupportedException("Method {$method} not supported"); - } - - if ($this->verify_peer) { - $options = array_merge( - $options, - [ - 'ssl' => [ - 'verify_peer' => true, - 'capath' => $this->capath, - 'cafile' => $this->cainfo, - ] - ] - ); - } - - $context = stream_context_create($options); - return file_get_contents($url, false, $context); - } - - protected function sendRequest($url, $method = 'GET', $params = []) - { - if (function_exists('curl_init') && !ini_get('safe_mode')) { - return $this->sendCurlRequest($url, $method, $params); - } - return $this->sendStreamRequest($url, $method, $params); - } - - protected function buildUrl($url, $parts) - { - if (isset($url['query'], $parts['query'])) { - $parts['query'] = $url['query'] . '&' . $parts['query']; - } - - $url = $parts + $url; - $url = $url['scheme'] . '://' - . (empty($url['username']) ? '' - :(empty($url['password']) ? "{$url['username']}@" - :"{$url['username']}:{$url['password']}@")) - . $url['host'] - . (empty($url['port']) ? '' : ":{$url['port']}") - . (empty($url['path']) ? '' : $url['path']) - . (empty($url['query']) ? '' : "?{$url['query']}") - . (empty($url['fragment']) ? '' : "#{$url['fragment']}"); - return $url; - } - - /** - * Helper function used to scan for / tags and extract information - * from them - */ - protected function extractHtmlTagValue($content, $tag, $attrName, $attrValue, $valueName) - { - preg_match_all("#<{$tag}[^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*$valueName=['\"](.+?)['\"][^>]*/?>#i", $content, $matches1); - preg_match_all("#<{$tag}[^>]*$valueName=['\"](.+?)['\"][^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*/?>#i", $content, $matches2); - - $result = array_merge($matches1[1], $matches2[1]); - return empty($result) ? false : $result[0]; - } - - /** - * Performs Yadis and HTML discovery. Normally not used. - * @param string $url Identity URL. - * @return string OP Endpoint (i.e. OpenID provider address). - * @throws Exception - */ - public function discover($url) - { - if (!$url) { - throw new Exception('No identity supplied.'); - } - # Use xri.net proxy to resolve i-name identities - if (!preg_match('#^https?:#', $url)) { - $url = "https://xri.net/$url"; - } - - # We save the original url in case of Yadis discovery failure. - # It can happen when we'll be lead to an XRDS document - # which does not have any OpenID2 services. - $originalUrl = $url; - - # A flag to disable yadis discovery in case of failure in headers. - $yadis = true; - - # We'll jump a maximum of 5 times, to avoid endless redirections. - for ($i = 0; $i < 5; $i ++) { - if ($yadis) { - $headers = $this->sendRequest($url, 'HEAD'); - - $next = false; - if (isset($headers['x-xrds-location'])) { - $url = $this->buildUrl(parse_url($url), parse_url(trim($headers['x-xrds-location']))); - $next = true; - } - - if (isset($headers['content-type']) - && (strpos($headers['content-type'], 'application/xrds+xml') !== false - || strpos($headers['content-type'], 'text/xml') !== false) - ) { - # Apparently, some providers return XRDS documents as text/html. - # While it is against the spec, allowing this here shouldn't break - # compatibility with anything. - # --- - # Found an XRDS document, now let's find the server, and optionally delegate. - $content = $this->sendRequest($url, 'GET'); - - preg_match_all('#(.*?)#s', $content, $m); - foreach ($m[1] as $content) { - $content = ' ' . $content; # The space is added, so that strpos doesn't return 0. - - # OpenID 2 - $ns = preg_quote('http://specs.openid.net/auth/2.0/'); - if (preg_match('#\s*'.$ns.'(server|signon)\s*#s', $content, $type)) { - if ($type[1] == 'server') { - $this->identifier_select = true; - } - - preg_match('#(.*)#', $content, $server); - preg_match('#<(Local|Canonical)ID>(.*)#', $content, $delegate); - if (empty($server)) { - return false; - } - # Does the server advertise support for either AX or SREG? - $this->ax = (bool) strpos($content, 'http://openid.net/srv/ax/1.0'); - $this->sreg = strpos($content, 'http://openid.net/sreg/1.0') - || strpos($content, 'http://openid.net/extensions/sreg/1.1'); - - $server = $server[1]; - if (isset($delegate[2])) { - $this->identity = trim($delegate[2]); - } - $this->version = 2; - - $this->server = $server; - return $server; - } - - # OpenID 1.1 - $ns = preg_quote('http://openid.net/signon/1.1'); - if (preg_match('#\s*'.$ns.'\s*#s', $content)) { - - preg_match('#(.*)#', $content, $server); - preg_match('#<.*?Delegate>(.*)#', $content, $delegate); - if (empty($server)) { - return false; - } - # AX can be used only with OpenID 2.0, so checking only SREG - $this->sreg = strpos($content, 'http://openid.net/sreg/1.0') - || strpos($content, 'http://openid.net/extensions/sreg/1.1'); - - $server = $server[1]; - if (isset($delegate[1])) { - $this->identity = $delegate[1]; - } - $this->version = 1; - - $this->server = $server; - return $server; - } - } - - $next = true; - $yadis = false; - $url = $originalUrl; - $content = null; - break; - } - if ($next) { - continue; - } - - # There are no relevant information in headers, so we search the body. - $content = $this->sendRequest($url, 'GET'); - $location = $this->extractHtmlTagValue($content, 'meta', 'http-equiv', 'X-XRDS-Location', 'content'); - if ($location) { - $url = $this->buildUrl(parse_url($url), parse_url($location)); - continue; - } - } - - if (!isset($content)) { - $content = $this->sendRequest($url, 'GET'); - } - - # At this point, the YADIS Discovery has failed, so we'll switch - # to openid2 HTML discovery, then fallback to openid 1.1 discovery. - $server = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid2.provider', 'href'); - $delegate = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid2.local_id', 'href'); - $this->version = 2; - - if (!$server) { - # The same with openid 1.1 - $server = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid.server', 'href'); - $delegate = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid.delegate', 'href'); - $this->version = 1; - } - - if ($server) { - # We found an OpenID2 OP Endpoint - if ($delegate) { - # We have also found an OP-Local ID. - $this->identity = $delegate; - } - $this->server = $server; - return $server; - } - throw new Exception('No servers found!'); - } - throw new Exception('Endless redirection!'); - } - - protected function sregParams() - { - $params = []; - # We always use SREG 1.1, even if the server is advertising only support for 1.0. - # That's because it's fully backwards compatibile with 1.0, and some providers - # advertise 1.0 even if they accept only 1.1. One such provider is myopenid.com - $params['openid.ns.sreg'] = 'http://openid.net/extensions/sreg/1.1'; - if ($this->required) { - $params['openid.sreg.required'] = []; - foreach ($this->required as $required) { - if (!isset(self::$axToSregMap[$required])) { - continue; - } - $params['openid.sreg.required'][] = self::$axToSregMap[$required]; - } - $params['openid.sreg.required'] = implode(',', $params['openid.sreg.required']); - } - - if ($this->optional) { - $params['openid.sreg.optional'] = []; - foreach ($this->optional as $optional) { - if (!isset(self::$axToSregMap[$optional])) { - continue; - } - $params['openid.sreg.optional'][] = self::$axToSregMap[$optional]; - } - $params['openid.sreg.optional'] = implode(',', $params['openid.sreg.optional']); - } - return $params; - } - - protected function axParams() - { - $params = []; - if ($this->required || $this->optional) { - $params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0'; - $params['openid.ax.mode'] = 'fetch_request'; - $this->aliases = []; - $counts = []; - $required = []; - $optional = []; - foreach (['required', 'optional'] as $type) { - foreach ($this->$type as $alias => $field) { - if (is_int($alias)) { - $alias = strtr($field, '/', '_'); - } - $this->aliases[$alias] = 'http://axschema.org/' . $field; - if (empty($counts[$alias])) { - $counts[$alias] = 0; - } - $counts[$alias] += 1; - ${$type}[] = $alias; - } - } - foreach ($this->aliases as $alias => $ns) { - $params['openid.ax.type.' . $alias] = $ns; - } - foreach ($counts as $alias => $count) { - if ($count == 1) { - continue; - } - $params['openid.ax.count.' . $alias] = $count; - } - - # Don't send empty ax.requied and ax.if_available. - # Google and possibly other providers refuse to support ax when one of these is empty. - if ($required) { - $params['openid.ax.required'] = implode(',', $required); - } - if ($optional) { - $params['openid.ax.if_available'] = implode(',', $optional); - } - } - return $params; - } - - protected function authUrlV1() - { - $returnUrl = $this->returnUrl; - # If we have an openid.delegate that is different from our claimed id, - # we need to somehow preserve the claimed id between requests. - # The simplest way is to just send it along with the return_to url. - if ($this->identity != $this->claimed_id) { - $returnUrl .= (strpos($returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $this->claimed_id; - } - - $params = array_merge( - $this->sregParams(), - [ - 'openid.return_to' => $returnUrl, - 'openid.mode' => 'checkid_setup', - 'openid.identity' => $this->identity, - 'openid.trust_root' => $this->trustRoot, - ] - ); - - return $this->buildUrl(parse_url($this->server), ['query' => http_build_query($params, '', '&')]); - } - - protected function authUrlV2($identifierSelect) - { - $params = [ - 'openid.ns' => 'http://specs.openid.net/auth/2.0', - 'openid.mode' => 'checkid_setup', - 'openid.return_to' => $this->returnUrl, - 'openid.realm' => $this->trustRoot, - ]; - if ($this->ax) { - $params = array_merge($this->axParams(), $params); - } - if ($this->sreg) { - $params = array_merge($this->sregParams(), $params); - } - if (!$this->ax && !$this->sreg) { - # If OP doesn't advertise either SREG, nor AX, let's send them both - # in worst case we don't get anything in return. - $params = array_merge($this->sregParams(), $this->axParams(), $params); - } - - if ($identifierSelect) { - $url = 'http://specs.openid.net/auth/2.0/identifier_select'; - $params['openid.identity'] = $url; - $params['openid.claimed_id']= $url; - } else { - $params['openid.identity'] = $this->identity; - $params['openid.claimed_id'] = $this->claimed_id; - } - - return $this->buildUrl(parse_url($this->server), ['query' => http_build_query($params, '', '&')]); - } - - /** - * Returns authentication URL. Usually, you want to redirect your user to it. - * @param string $identifier_select Whether to request OP to select identity for an user in OpenID 2. Does not affect OpenID 1. - * @return string the authentication URL. - * @throws Exception - */ - public function authUrl($identifier_select = null) - { - if (!$this->server) { - $this->discover($this->identity); - } - if ($this->version == 2) { - if ($identifier_select === null) { - return $this->authUrlV2($this->identifier_select); - } - return $this->authUrlV2($identifier_select); - } - return $this->authUrlV1(); - } - - /** - * Performs OpenID verification with the OP. - * @return boolean whether the verification was successful. - * @throws Exception - */ - public function validate() - { - $this->claimed_id = isset($this->data['openid_claimed_id']) ? $this->data['openid_claimed_id'] : $this->data['openid_identity']; - $params = [ - 'openid.assoc_handle' => $this->data['openid_assoc_handle'], - 'openid.signed' => $this->data['openid_signed'], - 'openid.sig' => $this->data['openid_sig'], - ]; - - if (isset($this->data['openid_ns'])) { - # We're dealing with an OpenID 2.0 server, so let's set an ns - # Even though we should know location of the endpoint, - # we still need to verify it by discovery, so $server is not set here - $params['openid.ns'] = 'http://specs.openid.net/auth/2.0'; - } elseif (isset($this->data['openid_claimed_id']) - && $this->data['openid_claimed_id'] != $this->data['openid_identity'] - ) { - # If it's an OpenID 1 provider, and we've got claimed_id, - # we have to append it to the returnUrl, like authUrl_v1 does. - $this->returnUrl .= (strpos($this->returnUrl, '?') ? '&' : '?') - . 'openid.claimed_id=' . $this->claimed_id; - } - - if ($this->data['openid_return_to'] != $this->returnUrl) { - # The return_to url must match the url of current request. - # I'm assuing that noone will set the returnUrl to something that doesn't make sense. - return false; - } - - $server = $this->discover($this->claimed_id); - - foreach (explode(',', $this->data['openid_signed']) as $item) { - # Checking whether magic_quotes_gpc is turned on, because - # the function may fail if it is. For example, when fetching - # AX namePerson, it might containg an apostrophe, which will be escaped. - # In such case, validation would fail, since we'd send different data than OP - # wants to verify. stripslashes() should solve that problem, but we can't - # use it when magic_quotes is off. - $value = $this->data['openid_' . str_replace('.', '_', $item)]; - $params['openid.' . $item] = get_magic_quotes_gpc() ? stripslashes($value) : $value; - } - - $params['openid.mode'] = 'check_authentication'; - - $response = $this->sendRequest($server, 'POST', $params); - - return preg_match('/is_valid\s*:\s*true/i', $response); - } - - protected function getAxAttributes() - { - $alias = null; - if (isset($this->data['openid_ns_ax']) && $this->data['openid_ns_ax'] != 'http://openid.net/srv/ax/1.0') { - # It's the most likely case, so we'll check it before - $alias = 'ax'; - } else { - # 'ax' prefix is either undefined, or points to another extension, - # so we search for another prefix - foreach ($this->data as $key => $value) { - if (substr($key, 0, strlen('openid_ns_')) == 'openid_ns_' && $value == 'http://openid.net/srv/ax/1.0') { - $alias = substr($key, strlen('openid_ns_')); - break; - } - } - } - if (!$alias) { - # An alias for AX schema has not been found, - # so there is no AX data in the OP's response - return []; - } - - $attributes = []; - foreach ($this->data as $key => $value) { - $keyMatch = 'openid_' . $alias . '_value_'; - if (substr($key, 0, strlen($keyMatch)) != $keyMatch) { - continue; - } - $key = substr($key, strlen($keyMatch)); - if (!isset($this->data['openid_' . $alias . '_type_' . $key])) { - # OP is breaking the spec by returning a field without - # associated ns. This shouldn't happen, but it's better - # to check, than cause an E_NOTICE. - continue; - } - $key = substr($this->data['openid_' . $alias . '_type_' . $key], strlen('http://axschema.org/')); - $attributes[$key] = $value; - } - return $attributes; - } - - protected function getSregAttributes() - { - $attributes = array(); - $sregToAx = array_flip(self::$axToSregMap); - foreach ($this->data as $key => $value) { - $keyMatch = 'openid_sreg_'; - if (substr($key, 0, strlen($keyMatch)) != $keyMatch) { - continue; - } - $key = substr($key, strlen($keyMatch)); - if (!isset($sregToAx[$key])) { - # The field name isn't part of the SREG spec, so we ignore it. - continue; - } - $attributes[$sregToAx[$key]] = $value; - } - return $attributes; - } - - /** - * Gets AX/SREG attributes provided by OP. should be used only after successful validaton. - * Note that it does not guarantee that any of the required/optional parameters will be present, - * or that there will be no other attributes besides those specified. - * In other words. OP may provide whatever information it wants to. - * SREG names will be mapped to AX names. - * @return array array of attributes with keys being the AX schema names, e.g. 'contact/email' - * @see http://www.axschema.org/types/ - */ - public function getAttributes() - { - if (isset($this->data['openid_ns']) && $this->data['openid_ns'] == 'http://specs.openid.net/auth/2.0') { - # OpenID 2.0 - # We search for both AX and SREG attributes, with AX taking precedence. - return array_merge($this->getSregAttributes(), $this->getAxAttributes()); - } - return $this->getSregAttributes(); - } -} \ No newline at end of file diff --git a/extensions/yii/authclient/provider/OpenId.php b/extensions/yii/authclient/provider/OpenId.php new file mode 100644 index 0000000..ad00a39 --- /dev/null +++ b/extensions/yii/authclient/provider/OpenId.php @@ -0,0 +1,30 @@ + + * @since 2.0 + */ +class OpenId extends Client implements ProviderInterface +{ + use ProviderTrait; + + /** + * Authenticate the user. + * @return boolean whether user was successfully authenticated. + */ + public function authenticate() + { + // TODO: Implement authenticate() method. + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/provider/ProviderInterface.php b/extensions/yii/authclient/provider/ProviderInterface.php new file mode 100644 index 0000000..9be8a2f --- /dev/null +++ b/extensions/yii/authclient/provider/ProviderInterface.php @@ -0,0 +1,73 @@ + + * @since 2.0 + */ +interface ProviderInterface +{ + /** + * @param string $id service id. + */ + public function setId($id); + + /** + * @return string service id + */ + public function getId(); + + /** + * @return string service name. + */ + public function getName(); + + /** + * @param string $name service name. + */ + public function setName($name); + + /** + * @return string service title. + */ + public function getTitle(); + + /** + * @param string $title service title. + */ + public function setTitle($title); + + /** + * @param string $url successful URL. + */ + public function setSuccessUrl($url); + + /** + * @return string successful URL. + */ + public function getSuccessUrl(); + + /** + * @param string $url cancel URL. + */ + public function setCancelUrl($url); + + /** + * @return string cancel URL. + */ + public function getCancelUrl(); + + /** + * Authenticate the user. + * @return boolean whether user was successfully authenticated. + */ + public function authenticate(); +} \ No newline at end of file diff --git a/extensions/yii/authclient/provider/ProviderTrait.php b/extensions/yii/authclient/provider/ProviderTrait.php new file mode 100644 index 0000000..5926302 --- /dev/null +++ b/extensions/yii/authclient/provider/ProviderTrait.php @@ -0,0 +1,176 @@ + + * @since 2.0 + */ +trait ProviderTrait +{ + /** + * @var string service id. + * This value mainly used as HTTP request parameter. + */ + private $_id; + /** + * @var string service unique name. + * This value may be used in database records, CSS files and so on. + */ + private $_name; + /** + * @var string service title to display in views. + */ + private $_title; + /** + * @var string the redirect url after successful authorization. + */ + private $_successUrl = ''; + /** + * @var string the redirect url after unsuccessful authorization (e.g. user canceled). + */ + private $_cancelUrl = ''; + + /** + * @param string $id service id. + */ + public function setId($id) + { + $this->_id = $id; + } + + /** + * @return string service id + */ + public function getId() + { + if (empty($this->_id)) { + $this->_id = $this->getName(); + } + return $this->_id; + } + + /** + * @return string service name. + */ + public function getName() + { + if ($this->_name === null) { + $this->_name = $this->defaultName(); + } + return $this->_name; + } + + /** + * @param string $name service name. + */ + public function setName($name) + { + $this->_name = $name; + } + + /** + * @return string service title. + */ + public function getTitle() + { + if ($this->_title === null) { + $this->_title = $this->defaultTitle(); + } + return $this->_title; + } + + /** + * @param string $title service title. + */ + public function setTitle($title) + { + $this->_title = $title; + } + + /** + * @param string $url successful URL. + */ + public function setSuccessUrl($url) + { + $this->_successUrl = $url; + } + + /** + * @return string successful URL. + */ + public function getSuccessUrl() + { + if (empty($this->_successUrl)) { + $this->_successUrl = $this->defaultSuccessUrl(); + } + return $this->_successUrl; + } + + /** + * @param string $url cancel URL. + */ + public function setCancelUrl($url) + { + $this->_cancelUrl = $url; + } + + /** + * @return string cancel URL. + */ + public function getCancelUrl() + { + if (empty($this->_cancelUrl)) { + $this->_cancelUrl = $this->defaultCancelUrl(); + } + return $this->_cancelUrl; + } + + /** + * Generates service name. + * @return string service name. + */ + protected function defaultName() + { + return StringHelper::basename(get_class($this)); + } + + /** + * Generates service title. + * @return string service title. + */ + protected function defaultTitle() + { + return StringHelper::basename(get_class($this)); + } + + /** + * Creates default {@link successUrl} value. + * @return string success URL value. + */ + protected function defaultSuccessUrl() + { + return Yii::$app->getUser()->getReturnUrl(); + } + + /** + * Creates default {@link cancelUrl} value. + * @return string cancel URL value. + */ + protected function defaultCancelUrl() + { + return Yii::$app->getRequest()->getAbsoluteUrl(); + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/signature/BaseMethod.php b/extensions/yii/authclient/signature/BaseMethod.php new file mode 100644 index 0000000..d22242f --- /dev/null +++ b/extensions/yii/authclient/signature/BaseMethod.php @@ -0,0 +1,51 @@ + + * @since 2.0 + */ +abstract class BaseMethod extends Object +{ + /** + * Return the canonical name of the Signature Method. + * @return string method name. + */ + abstract public function getName(); + + /** + * Generates OAuth request signature. + * @param string $baseString signature base string. + * @param string $key signature key. + * @return string signature string. + */ + abstract public function generateSignature($baseString, $key); + + /** + * Verifies given OAuth request. + * @param string $signature signature to be verified. + * @param string $baseString signature base string. + * @param string $key signature key. + * @return boolean success. + */ + public function verify($signature, $baseString, $key) + { + $expectedSignature = $this->generateSignature($baseString, $key); + if (empty($signature) || empty($expectedSignature)) { + return false; + } + return (strcmp($expectedSignature, $signature) === 0); + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/signature/HmacSha1.php b/extensions/yii/authclient/signature/HmacSha1.php new file mode 100644 index 0000000..3f88435 --- /dev/null +++ b/extensions/yii/authclient/signature/HmacSha1.php @@ -0,0 +1,47 @@ + + * @since 2.0 + */ +class HmacSha1 extends BaseMethod +{ + /** + * @inheritdoc + */ + public function init() + { + if (!function_exists('hash_hmac')) { + throw new NotSupportedException('PHP "Hash" extension is required.'); + } + } + + /** + * @inheritdoc + */ + public function getName() + { + return 'HMAC-SHA1'; + } + + /** + * @inheritdoc + */ + public function generateSignature($baseString, $key) + { + return base64_encode(hash_hmac('sha1', $baseString, $key, true)); + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/signature/PlainText.php b/extensions/yii/authclient/signature/PlainText.php new file mode 100644 index 0000000..867c319 --- /dev/null +++ b/extensions/yii/authclient/signature/PlainText.php @@ -0,0 +1,33 @@ + + * @since 2.0 + */ +class PlainText extends BaseMethod +{ + /** + * @inheritdoc + */ + public function getName() + { + return 'PLAINTEXT'; + } + + /** + * @inheritdoc + */ + public function generateSignature($baseString, $key) + { + return $key; + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/signature/RsaSha1.php b/extensions/yii/authclient/signature/RsaSha1.php new file mode 100644 index 0000000..85d5485 --- /dev/null +++ b/extensions/yii/authclient/signature/RsaSha1.php @@ -0,0 +1,168 @@ + + * @since 2.0 + */ +class RsaSha1 extends BaseMethod +{ + /** + * @var string OpenSSL private key certificate content. + * This value can be fetched from file specified by {@link privateCertificateFile}. + */ + protected $_privateCertificate; + /** + * @var string OpenSSL public key certificate content. + * This value can be fetched from file specified by {@link publicCertificateFile}. + */ + protected $_publicCertificate; + /** + * @var string path to the file, which holds private key certificate. + */ + public $privateCertificateFile = ''; + /** + * @var string path to the file, which holds public key certificate. + */ + public $publicCertificateFile = ''; + + /** + * @inheritdoc + */ + public function init() + { + if (!function_exists('openssl_sign')) { + throw new NotSupportedException('PHP "OpenSSL" extension is required.'); + } + } + + /** + * @param string $publicCertificate public key certificate content. + */ + public function setPublicCertificate($publicCertificate) + { + $this->_publicCertificate = $publicCertificate; + } + + /** + * @return string public key certificate content. + */ + public function getPublicCertificate() + { + if ($this->_publicCertificate === null) { + $this->_publicCertificate = $this->initPublicCertificate(); + } + return $this->_publicCertificate; + } + + /** + * @param string $privateCertificate private key certificate content. + */ + public function setPrivateCertificate($privateCertificate) + { + $this->_privateCertificate = $privateCertificate; + } + + /** + * @return string private key certificate content. + */ + public function getPrivateCertificate() + { + if ($this->_privateCertificate === null) { + $this->_privateCertificate = $this->initPrivateCertificate(); + } + return $this->_privateCertificate; + } + + /** + * @inheritdoc + */ + public function getName() + { + return 'RSA-SHA1'; + } + + /** + * Creates initial value for {@link publicCertificate}. + * This method will attempt to fetch the certificate value from {@link publicCertificateFile} file. + * @throws InvalidConfigException on failure. + * @return string public certificate content. + */ + protected function initPublicCertificate() + { + if (!empty($this->publicCertificateFile)) { + if (!file_exists($this->publicCertificateFile)) { + throw new InvalidConfigException("Public certificate file '{$this->publicCertificateFile}' does not exist!"); + } + return file_get_contents($this->publicCertificateFile); + } else { + return ''; + } + } + + /** + * Creates initial value for {@link privateCertificate}. + * This method will attempt to fetch the certificate value from {@link privateCertificateFile} file. + * @throws InvalidConfigException on failure. + * @return string private certificate content. + */ + protected function initPrivateCertificate() + { + if (!empty($this->privateCertificateFile)) { + if (!file_exists($this->privateCertificateFile)) { + throw new InvalidConfigException("Private certificate file '{$this->privateCertificateFile}' does not exist!"); + } + return file_get_contents($this->privateCertificateFile); + } else { + return ''; + } + } + + /** + * @inheritdoc + */ + public function generateSignature($baseString, $key) + { + $privateCertificateContent = $this->getPrivateCertificate(); + // Pull the private key ID from the certificate + $privateKeyId = openssl_pkey_get_private($privateCertificateContent); + // Sign using the key + openssl_sign($baseString, $signature, $privateKeyId); + // Release the key resource + openssl_free_key($privateKeyId); + return base64_encode($signature); + } + + /** + * @inheritdoc + */ + public function verify($signature, $baseString, $key) + { + $decodedSignature = base64_decode($signature); + // Fetch the public key cert based on the request + $publicCertificate = $this->getPublicCertificate(); + // Pull the public key ID from the certificate + $publicKeyId = openssl_pkey_get_public($publicCertificate); + // Check the computed signature against the one passed in the query + $verificationResult = openssl_verify($baseString, $decodedSignature, $publicKeyId); + // Release the key resource + openssl_free_key($publicKeyId); + return ($verificationResult == 1); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/BaseOAuthTest.php b/tests/unit/extensions/authclient/BaseOAuthTest.php new file mode 100644 index 0000000..00893b5 --- /dev/null +++ b/tests/unit/extensions/authclient/BaseOAuthTest.php @@ -0,0 +1,251 @@ +getMock(BaseOAuth::className(), ['setState', 'getState', 'composeRequestCurlOptions', 'refreshAccessToken', 'apiInternal']); + $oauthClient->expects($this->any())->method('setState')->will($this->returnValue($oauthClient)); + $oauthClient->expects($this->any())->method('getState')->will($this->returnValue(null)); + return $oauthClient; + } + + /** + * Invokes the OAuth client method even if it is protected. + * @param BaseOAuth $oauthClient OAuth client instance. + * @param string $methodName name of the method to be invoked. + * @param array $arguments method arguments. + * @return mixed method invoke result. + */ + protected function invokeOAuthClientMethod($oauthClient, $methodName, array $arguments = []) + { + $classReflection = new \ReflectionClass(get_class($oauthClient)); + $methodReflection = $classReflection->getMethod($methodName); + $methodReflection->setAccessible(true); + $result = $methodReflection->invokeArgs($oauthClient, $arguments); + $methodReflection->setAccessible(false); + return $result; + } + + // Tests : + + public function testSetGet() + { + $oauthClient = $this->createOAuthClient(); + + $returnUrl = 'http://test.return.url'; + $oauthClient->setReturnUrl($returnUrl); + $this->assertEquals($returnUrl, $oauthClient->getReturnUrl(), 'Unable to setup return URL!'); + + $curlOptions = [ + 'option1' => 'value1', + 'option2' => 'value2', + ]; + $oauthClient->setCurlOptions($curlOptions); + $this->assertEquals($curlOptions, $oauthClient->getCurlOptions(), 'Unable to setup cURL options!'); + } + + public function testSetupComponents() + { + $oauthClient = $this->createOAuthClient(); + + $oauthToken = new OAuthToken(); + $oauthClient->setAccessToken($oauthToken); + $this->assertEquals($oauthToken, $oauthClient->getAccessToken(), 'Unable to setup token!'); + + $oauthSignatureMethod = new PlainText(); + $oauthClient->setSignatureMethod($oauthSignatureMethod); + $this->assertEquals($oauthSignatureMethod, $oauthClient->getSignatureMethod(), 'Unable to setup signature method!'); + } + + /** + * @depends testSetupComponents + */ + public function testSetupComponentsByConfig() + { + $oauthClient = $this->createOAuthClient(); + + $oauthToken = [ + 'token' => 'test_token', + 'tokenSecret' => 'test_token_secret', + ]; + $oauthClient->setAccessToken($oauthToken); + $this->assertEquals($oauthToken['token'], $oauthClient->getAccessToken()->getToken(), 'Unable to setup token as config!'); + + $oauthSignatureMethod = [ + 'class' => 'yii\authclient\signature\PlainText' + ]; + $oauthClient->setSignatureMethod($oauthSignatureMethod); + $returnedSignatureMethod = $oauthClient->getSignatureMethod(); + $this->assertEquals($oauthSignatureMethod['class'], get_class($returnedSignatureMethod), 'Unable to setup signature method as config!'); + } + + /** + * Data provider for [[testComposeUrl()]]. + * @return array test data. + */ + public function composeUrlDataProvider() + { + return [ + [ + 'http://test.url', + [ + 'param1' => 'value1', + 'param2' => 'value2', + ], + 'http://test.url?param1=value1¶m2=value2', + ], + [ + 'http://test.url?with=some', + [ + 'param1' => 'value1', + 'param2' => 'value2', + ], + 'http://test.url?with=some¶m1=value1¶m2=value2', + ], + ]; + } + + /** + * @dataProvider composeUrlDataProvider + * + * @param string $url request URL. + * @param array $params request params + * @param string $expectedUrl expected composed URL. + */ + public function testComposeUrl($url, array $params, $expectedUrl) + { + $oauthClient = $this->createOAuthClient(); + $composedUrl = $this->invokeOAuthClientMethod($oauthClient, 'composeUrl', [$url, $params]); + $this->assertEquals($expectedUrl, $composedUrl); + } + + /** + * Data provider for {@link testDetermineContentTypeByHeaders}. + * @return array test data. + */ + public function determineContentTypeByHeadersDataProvider() + { + return [ + [ + ['content_type' => 'application/json'], + 'json' + ], + [ + ['content_type' => 'application/x-www-form-urlencoded'], + 'urlencoded' + ], + [ + ['content_type' => 'application/xml'], + 'xml' + ], + [ + ['some_header' => 'some_header_value'], + 'auto' + ], + [ + ['content_type' => 'unknown'], + 'auto' + ], + ]; + } + + /** + * @dataProvider determineContentTypeByHeadersDataProvider + * + * @param array $headers request headers. + * @param string $expectedResponseType expected response type. + */ + public function testDetermineContentTypeByHeaders(array $headers, $expectedResponseType) + { + $oauthClient = $this->createOAuthClient(); + $responseType = $this->invokeOAuthClientMethod($oauthClient, 'determineContentTypeByHeaders', [$headers]); + $this->assertEquals($expectedResponseType, $responseType); + } + + /** + * Data provider for [[testDetermineContentTypeByRaw]]. + * @return array test data. + */ + public function determineContentTypeByRawDataProvider() + { + return array( + ['{name: value}', 'json'], + ['name=value', 'urlencoded'], + ['name1=value1&name2=value2', 'urlencoded'], + ['Value', 'xml'], + ['Value', 'xml'], + ); + } + + /** + * @dataProvider determineContentTypeByRawDataProvider + * + * @param string $rawResponse raw response content. + * @param string $expectedResponseType expected response type. + */ + public function testDetermineContentTypeByRaw($rawResponse, $expectedResponseType) + { + $oauthClient = $this->createOAuthClient(); + $responseType = $this->invokeOAuthClientMethod($oauthClient, 'determineContentTypeByRaw', [$rawResponse]); + $this->assertEquals($expectedResponseType, $responseType); + } + + /** + * Data provider for [[testApiUrl]]. + * @return array test data. + */ + public function apiUrlDataProvider() + { + return [ + [ + 'http://api.base.url', + 'sub/url', + 'http://api.base.url/sub/url', + ], + [ + 'http://api.base.url', + 'http://api.base.url/sub/url', + 'http://api.base.url/sub/url', + ], + [ + 'http://api.base.url', + 'https://api.base.url/sub/url', + 'https://api.base.url/sub/url', + ], + ]; + } + + /** + * @dataProvider apiUrlDataProvider + * + * @param $apiBaseUrl + * @param $apiSubUrl + * @param $expectedApiFullUrl + */ + public function testApiUrl($apiBaseUrl, $apiSubUrl, $expectedApiFullUrl) + { + $oauthClient = $this->createOAuthClient(); + $oauthClient->expects($this->any())->method('apiInternal')->will($this->returnArgument(1)); + + $accessToken = new OAuthToken(); + $accessToken->setToken('test_access_token'); + $accessToken->setExpireDuration(1000); + $oauthClient->setAccessToken($accessToken); + + $oauthClient->apiBaseUrl = $apiBaseUrl; + + $this->assertEquals($expectedApiFullUrl, $oauthClient->api($apiSubUrl)); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/OAuth1Test.php b/tests/unit/extensions/authclient/OAuth1Test.php new file mode 100644 index 0000000..e5fe8fe --- /dev/null +++ b/tests/unit/extensions/authclient/OAuth1Test.php @@ -0,0 +1,109 @@ +mockApplication([], '\yii\web\Application'); + } + + /** + * Invokes the OAuth client method even if it is protected. + * @param OAuth1 $oauthClient OAuth client instance. + * @param string $methodName name of the method to be invoked. + * @param array $arguments method arguments. + * @return mixed method invoke result. + */ + protected function invokeOAuthClientMethod($oauthClient, $methodName, array $arguments = []) + { + $classReflection = new \ReflectionClass(get_class($oauthClient)); + $methodReflection = $classReflection->getMethod($methodName); + $methodReflection->setAccessible(true); + $result = $methodReflection->invokeArgs($oauthClient, $arguments); + $methodReflection->setAccessible(false); + return $result; + } + + // Tests : + + public function testSignRequest() + { + $oauthClient = new OAuth1(); + + $oauthSignatureMethod = new PlainText(); + $oauthClient->setSignatureMethod($oauthSignatureMethod); + + $signedParams = $this->invokeOAuthClientMethod($oauthClient, 'signRequest', ['GET', 'http://test.url', []]); + $this->assertNotEmpty($signedParams['oauth_signature'], 'Unable to sign request!'); + } + + /** + * Data provider for [[testComposeAuthorizationHeader()]]. + * @return array test data. + */ + public function composeAuthorizationHeaderDataProvider() + { + return [ + [ + '', + [ + 'oauth_test_name_1' => 'oauth_test_value_1', + 'oauth_test_name_2' => 'oauth_test_value_2', + ], + 'Authorization: OAuth oauth_test_name_1="oauth_test_value_1", oauth_test_name_2="oauth_test_value_2"' + ], + [ + 'test_realm', + [ + 'oauth_test_name_1' => 'oauth_test_value_1', + 'oauth_test_name_2' => 'oauth_test_value_2', + ], + 'Authorization: OAuth realm="test_realm", oauth_test_name_1="oauth_test_value_1", oauth_test_name_2="oauth_test_value_2"' + ], + [ + '', + [ + 'oauth_test_name_1' => 'oauth_test_value_1', + 'test_name_2' => 'test_value_2', + ], + 'Authorization: OAuth oauth_test_name_1="oauth_test_value_1"' + ], + ]; + } + + /** + * @dataProvider composeAuthorizationHeaderDataProvider + * + * @param string $realm authorization realm. + * @param array $params request params. + * @param string $expectedAuthorizationHeader expected authorization header. + */ + public function testComposeAuthorizationHeader($realm, array $params, $expectedAuthorizationHeader) + { + $oauthClient = new OAuth1(); + $authorizationHeader = $this->invokeOAuthClientMethod($oauthClient, 'composeAuthorizationHeader', [$params, $realm]); + $this->assertEquals($expectedAuthorizationHeader, $authorizationHeader); + } + + public function testBuildAuthUrl() { + $oauthClient = new OAuth1(); + $authUrl = 'http://test.auth.url'; + $oauthClient->authUrl = $authUrl; + + $requestTokenToken = 'test_request_token'; + $requestToken = new OAuthToken(); + $requestToken->setToken($requestTokenToken); + + $builtAuthUrl = $oauthClient->buildAuthUrl($requestToken); + + $this->assertContains($authUrl, $builtAuthUrl, 'No auth URL present!'); + $this->assertContains($requestTokenToken, $builtAuthUrl, 'No token present!'); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/OAuth2Test.php b/tests/unit/extensions/authclient/OAuth2Test.php new file mode 100644 index 0000000..69721cd --- /dev/null +++ b/tests/unit/extensions/authclient/OAuth2Test.php @@ -0,0 +1,33 @@ +mockApplication([], '\yii\web\Application'); + } + + // Tests : + + public function testBuildAuthUrl() + { + $oauthClient = new OAuth2(); + $authUrl = 'http://test.auth.url'; + $oauthClient->authUrl = $authUrl; + $clientId = 'test_client_id'; + $oauthClient->clientId = $clientId; + $returnUrl = 'http://test.return.url'; + $oauthClient->setReturnUrl($returnUrl); + + $builtAuthUrl = $oauthClient->buildAuthUrl(); + + $this->assertContains($authUrl, $builtAuthUrl, 'No auth URL present!'); + $this->assertContains($clientId, $builtAuthUrl, 'No client id present!'); + $this->assertContains(rawurlencode($returnUrl), $builtAuthUrl, 'No return URL present!'); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/TokenTest.php b/tests/unit/extensions/authclient/TokenTest.php new file mode 100644 index 0000000..3787d8a --- /dev/null +++ b/tests/unit/extensions/authclient/TokenTest.php @@ -0,0 +1,133 @@ + 'test_token_param_key', + 'tokenSecretParamKey' => 'test_token_secret_param_key', + ]; + $oauthToken = new OAuthToken($config); + $this->assertTrue(is_object($oauthToken), 'Unable to create access token!'); + foreach ($config as $name => $value) { + $this->assertEquals($value, $oauthToken->$name, 'Unable to setup attributes by constructor!'); + } + $this->assertTrue($oauthToken->createTimestamp > 0, 'Unable to fill create timestamp!'); + } + + public function testSetupParams() + { + $oauthToken = new OAuthToken(); + + $params = [ + 'name_1' => 'value_1', + 'name_2' => 'value_2', + ]; + $oauthToken->setParams($params); + $this->assertEquals($params, $oauthToken->getParams(), 'Unable to setup params!'); + + $newParamName = 'new_param_name'; + $newParamValue = 'new_param_value'; + $oauthToken->setParam($newParamName, $newParamValue); + $this->assertEquals($newParamValue, $oauthToken->getParam($newParamName), 'Unable to setup param by name!'); + } + + /** + * @depends testSetupParams + */ + public function testSetupParamsShortcuts() + { + $oauthToken = new OAuthToken(); + + $token = 'test_token_value'; + $oauthToken->setToken($token); + $this->assertEquals($token, $oauthToken->getToken(), 'Unable to setup token!'); + + $tokenSecret = 'test_token_secret'; + $oauthToken->setTokenSecret($tokenSecret); + $this->assertEquals($tokenSecret, $oauthToken->getTokenSecret(), 'Unable to setup token secret!'); + + $tokenExpireDuration = rand(1000, 2000); + $oauthToken->setExpireDuration($tokenExpireDuration); + $this->assertEquals($tokenExpireDuration, $oauthToken->getExpireDuration(), 'Unable to setup expire duration!'); + } + + /** + * Data provider for {@link testAutoFetchExpireDuration}. + * @return array test data. + */ + public function autoFetchExpireDurationDataProvider() + { + return [ + [ + ['expire_in' => 123345], + 123345 + ], + [ + ['expire' => 233456], + 233456 + ], + [ + ['expiry_in' => 34567], + 34567 + ], + [ + ['expiry' => 45678], + 45678 + ], + ]; + } + + /** + * @depends testSetupParamsShortcuts + * @dataProvider autoFetchExpireDurationDataProvider + * + * @param array $params + * @param $expectedExpireDuration + */ + public function testAutoFetchExpireDuration(array $params, $expectedExpireDuration) + { + $oauthToken = new OAuthToken(); + $oauthToken->setParams($params); + $this->assertEquals($expectedExpireDuration, $oauthToken->getExpireDuration()); + } + + /** + * @depends testSetupParamsShortcuts + */ + public function testGetIsExpired() + { + $oauthToken = new OAuthToken(); + $expireDuration = 3600; + $oauthToken->setExpireDuration($expireDuration); + + $this->assertFalse($oauthToken->getIsExpired(), 'Not expired token check fails!'); + + $oauthToken->createTimestamp = $oauthToken->createTimestamp - ($expireDuration +1); + $this->assertTrue($oauthToken->getIsExpired(), 'Expired token check fails!'); + } + + /** + * @depends testGetIsExpired + */ + public function testGetIsValid() + { + $oauthToken = new OAuthToken(); + $expireDuration = 3600; + $oauthToken->setExpireDuration($expireDuration); + + $this->assertFalse($oauthToken->getIsValid(), 'Empty token is valid!'); + + $oauthToken->setToken('test_token'); + $this->assertTrue($oauthToken->getIsValid(), 'Filled up token is invalid!'); + + $oauthToken->createTimestamp = $oauthToken->createTimestamp - ($expireDuration +1); + $this->assertFalse($oauthToken->getIsValid(), 'Expired token is valid!'); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/oauth/BaseClientTest.php b/tests/unit/extensions/authclient/oauth/BaseClientTest.php deleted file mode 100644 index e5085e7..0000000 --- a/tests/unit/extensions/authclient/oauth/BaseClientTest.php +++ /dev/null @@ -1,251 +0,0 @@ -getMock(BaseClient::className(), ['setState', 'getState', 'composeRequestCurlOptions', 'refreshAccessToken', 'apiInternal']); - $oauthClient->expects($this->any())->method('setState')->will($this->returnValue($oauthClient)); - $oauthClient->expects($this->any())->method('getState')->will($this->returnValue(null)); - return $oauthClient; - } - - /** - * Invokes the OAuth client method even if it is protected. - * @param BaseClient $oauthClient OAuth client instance. - * @param string $methodName name of the method to be invoked. - * @param array $arguments method arguments. - * @return mixed method invoke result. - */ - protected function invokeOAuthClientMethod($oauthClient, $methodName, array $arguments = []) - { - $classReflection = new \ReflectionClass(get_class($oauthClient)); - $methodReflection = $classReflection->getMethod($methodName); - $methodReflection->setAccessible(true); - $result = $methodReflection->invokeArgs($oauthClient, $arguments); - $methodReflection->setAccessible(false); - return $result; - } - - // Tests : - - public function testSetGet() - { - $oauthClient = $this->createOAuthClient(); - - $returnUrl = 'http://test.return.url'; - $oauthClient->setReturnUrl($returnUrl); - $this->assertEquals($returnUrl, $oauthClient->getReturnUrl(), 'Unable to setup return URL!'); - - $curlOptions = [ - 'option1' => 'value1', - 'option2' => 'value2', - ]; - $oauthClient->setCurlOptions($curlOptions); - $this->assertEquals($curlOptions, $oauthClient->getCurlOptions(), 'Unable to setup cURL options!'); - } - - public function testSetupComponents() - { - $oauthClient = $this->createOAuthClient(); - - $oauthToken = new Token(); - $oauthClient->setAccessToken($oauthToken); - $this->assertEquals($oauthToken, $oauthClient->getAccessToken(), 'Unable to setup token!'); - - $oauthSignatureMethod = new PlainText(); - $oauthClient->setSignatureMethod($oauthSignatureMethod); - $this->assertEquals($oauthSignatureMethod, $oauthClient->getSignatureMethod(), 'Unable to setup signature method!'); - } - - /** - * @depends testSetupComponents - */ - public function testSetupComponentsByConfig() - { - $oauthClient = $this->createOAuthClient(); - - $oauthToken = [ - 'token' => 'test_token', - 'tokenSecret' => 'test_token_secret', - ]; - $oauthClient->setAccessToken($oauthToken); - $this->assertEquals($oauthToken['token'], $oauthClient->getAccessToken()->getToken(), 'Unable to setup token as config!'); - - $oauthSignatureMethod = [ - 'class' => 'yii\authclient\oauth\signature\PlainText' - ]; - $oauthClient->setSignatureMethod($oauthSignatureMethod); - $returnedSignatureMethod = $oauthClient->getSignatureMethod(); - $this->assertEquals($oauthSignatureMethod['class'], get_class($returnedSignatureMethod), 'Unable to setup signature method as config!'); - } - - /** - * Data provider for [[testComposeUrl()]]. - * @return array test data. - */ - public function composeUrlDataProvider() - { - return [ - [ - 'http://test.url', - [ - 'param1' => 'value1', - 'param2' => 'value2', - ], - 'http://test.url?param1=value1¶m2=value2', - ], - [ - 'http://test.url?with=some', - [ - 'param1' => 'value1', - 'param2' => 'value2', - ], - 'http://test.url?with=some¶m1=value1¶m2=value2', - ], - ]; - } - - /** - * @dataProvider composeUrlDataProvider - * - * @param string $url request URL. - * @param array $params request params - * @param string $expectedUrl expected composed URL. - */ - public function testComposeUrl($url, array $params, $expectedUrl) - { - $oauthClient = $this->createOAuthClient(); - $composedUrl = $this->invokeOAuthClientMethod($oauthClient, 'composeUrl', [$url, $params]); - $this->assertEquals($expectedUrl, $composedUrl); - } - - /** - * Data provider for {@link testDetermineContentTypeByHeaders}. - * @return array test data. - */ - public function determineContentTypeByHeadersDataProvider() - { - return [ - [ - ['content_type' => 'application/json'], - 'json' - ], - [ - ['content_type' => 'application/x-www-form-urlencoded'], - 'urlencoded' - ], - [ - ['content_type' => 'application/xml'], - 'xml' - ], - [ - ['some_header' => 'some_header_value'], - 'auto' - ], - [ - ['content_type' => 'unknown'], - 'auto' - ], - ]; - } - - /** - * @dataProvider determineContentTypeByHeadersDataProvider - * - * @param array $headers request headers. - * @param string $expectedResponseType expected response type. - */ - public function testDetermineContentTypeByHeaders(array $headers, $expectedResponseType) - { - $oauthClient = $this->createOAuthClient(); - $responseType = $this->invokeOAuthClientMethod($oauthClient, 'determineContentTypeByHeaders', [$headers]); - $this->assertEquals($expectedResponseType, $responseType); - } - - /** - * Data provider for [[testDetermineContentTypeByRaw]]. - * @return array test data. - */ - public function determineContentTypeByRawDataProvider() - { - return array( - ['{name: value}', 'json'], - ['name=value', 'urlencoded'], - ['name1=value1&name2=value2', 'urlencoded'], - ['Value', 'xml'], - ['Value', 'xml'], - ); - } - - /** - * @dataProvider determineContentTypeByRawDataProvider - * - * @param string $rawResponse raw response content. - * @param string $expectedResponseType expected response type. - */ - public function testDetermineContentTypeByRaw($rawResponse, $expectedResponseType) - { - $oauthClient = $this->createOAuthClient(); - $responseType = $this->invokeOAuthClientMethod($oauthClient, 'determineContentTypeByRaw', [$rawResponse]); - $this->assertEquals($expectedResponseType, $responseType); - } - - /** - * Data provider for [[testApiUrl]]. - * @return array test data. - */ - public function apiUrlDataProvider() - { - return [ - [ - 'http://api.base.url', - 'sub/url', - 'http://api.base.url/sub/url', - ], - [ - 'http://api.base.url', - 'http://api.base.url/sub/url', - 'http://api.base.url/sub/url', - ], - [ - 'http://api.base.url', - 'https://api.base.url/sub/url', - 'https://api.base.url/sub/url', - ], - ]; - } - - /** - * @dataProvider apiUrlDataProvider - * - * @param $apiBaseUrl - * @param $apiSubUrl - * @param $expectedApiFullUrl - */ - public function testApiUrl($apiBaseUrl, $apiSubUrl, $expectedApiFullUrl) - { - $oauthClient = $this->createOAuthClient(); - $oauthClient->expects($this->any())->method('apiInternal')->will($this->returnArgument(1)); - - $accessToken = new Token(); - $accessToken->setToken('test_access_token'); - $accessToken->setExpireDuration(1000); - $oauthClient->setAccessToken($accessToken); - - $oauthClient->apiBaseUrl = $apiBaseUrl; - - $this->assertEquals($expectedApiFullUrl, $oauthClient->api($apiSubUrl)); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/oauth/Client1Test.php b/tests/unit/extensions/authclient/oauth/Client1Test.php deleted file mode 100644 index e882a68..0000000 --- a/tests/unit/extensions/authclient/oauth/Client1Test.php +++ /dev/null @@ -1,109 +0,0 @@ -mockApplication([], '\yii\web\Application'); - } - - /** - * Invokes the OAuth client method even if it is protected. - * @param Client1 $oauthClient OAuth client instance. - * @param string $methodName name of the method to be invoked. - * @param array $arguments method arguments. - * @return mixed method invoke result. - */ - protected function invokeOAuthClientMethod($oauthClient, $methodName, array $arguments = []) - { - $classReflection = new \ReflectionClass(get_class($oauthClient)); - $methodReflection = $classReflection->getMethod($methodName); - $methodReflection->setAccessible(true); - $result = $methodReflection->invokeArgs($oauthClient, $arguments); - $methodReflection->setAccessible(false); - return $result; - } - - // Tests : - - public function testSignRequest() - { - $oauthClient = new Client1(); - - $oauthSignatureMethod = new PlainText(); - $oauthClient->setSignatureMethod($oauthSignatureMethod); - - $signedParams = $this->invokeOAuthClientMethod($oauthClient, 'signRequest', ['GET', 'http://test.url', []]); - $this->assertNotEmpty($signedParams['oauth_signature'], 'Unable to sign request!'); - } - - /** - * Data provider for [[testComposeAuthorizationHeader()]]. - * @return array test data. - */ - public function composeAuthorizationHeaderDataProvider() - { - return [ - [ - '', - [ - 'oauth_test_name_1' => 'oauth_test_value_1', - 'oauth_test_name_2' => 'oauth_test_value_2', - ], - 'Authorization: OAuth oauth_test_name_1="oauth_test_value_1", oauth_test_name_2="oauth_test_value_2"' - ], - [ - 'test_realm', - [ - 'oauth_test_name_1' => 'oauth_test_value_1', - 'oauth_test_name_2' => 'oauth_test_value_2', - ], - 'Authorization: OAuth realm="test_realm", oauth_test_name_1="oauth_test_value_1", oauth_test_name_2="oauth_test_value_2"' - ], - [ - '', - [ - 'oauth_test_name_1' => 'oauth_test_value_1', - 'test_name_2' => 'test_value_2', - ], - 'Authorization: OAuth oauth_test_name_1="oauth_test_value_1"' - ], - ]; - } - - /** - * @dataProvider composeAuthorizationHeaderDataProvider - * - * @param string $realm authorization realm. - * @param array $params request params. - * @param string $expectedAuthorizationHeader expected authorization header. - */ - public function testComposeAuthorizationHeader($realm, array $params, $expectedAuthorizationHeader) - { - $oauthClient = new Client1(); - $authorizationHeader = $this->invokeOAuthClientMethod($oauthClient, 'composeAuthorizationHeader', [$params, $realm]); - $this->assertEquals($expectedAuthorizationHeader, $authorizationHeader); - } - - public function testBuildAuthUrl() { - $oauthClient = new Client1(); - $authUrl = 'http://test.auth.url'; - $oauthClient->authUrl = $authUrl; - - $requestTokenToken = 'test_request_token'; - $requestToken = new Token(); - $requestToken->setToken($requestTokenToken); - - $builtAuthUrl = $oauthClient->buildAuthUrl($requestToken); - - $this->assertContains($authUrl, $builtAuthUrl, 'No auth URL present!'); - $this->assertContains($requestTokenToken, $builtAuthUrl, 'No token present!'); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/oauth/Client2Test.php b/tests/unit/extensions/authclient/oauth/Client2Test.php deleted file mode 100644 index c8eeab8..0000000 --- a/tests/unit/extensions/authclient/oauth/Client2Test.php +++ /dev/null @@ -1,33 +0,0 @@ -mockApplication([], '\yii\web\Application'); - } - - // Tests : - - public function testBuildAuthUrl() - { - $oauthClient = new Client2(); - $authUrl = 'http://test.auth.url'; - $oauthClient->authUrl = $authUrl; - $clientId = 'test_client_id'; - $oauthClient->clientId = $clientId; - $returnUrl = 'http://test.return.url'; - $oauthClient->setReturnUrl($returnUrl); - - $builtAuthUrl = $oauthClient->buildAuthUrl(); - - $this->assertContains($authUrl, $builtAuthUrl, 'No auth URL present!'); - $this->assertContains($clientId, $builtAuthUrl, 'No client id present!'); - $this->assertContains(rawurlencode($returnUrl), $builtAuthUrl, 'No return URL present!'); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/oauth/TokenTest.php b/tests/unit/extensions/authclient/oauth/TokenTest.php deleted file mode 100644 index d319198..0000000 --- a/tests/unit/extensions/authclient/oauth/TokenTest.php +++ /dev/null @@ -1,133 +0,0 @@ - 'test_token_param_key', - 'tokenSecretParamKey' => 'test_token_secret_param_key', - ]; - $oauthToken = new Token($config); - $this->assertTrue(is_object($oauthToken), 'Unable to create access token!'); - foreach ($config as $name => $value) { - $this->assertEquals($value, $oauthToken->$name, 'Unable to setup attributes by constructor!'); - } - $this->assertTrue($oauthToken->createTimestamp > 0, 'Unable to fill create timestamp!'); - } - - public function testSetupParams() - { - $oauthToken = new Token(); - - $params = [ - 'name_1' => 'value_1', - 'name_2' => 'value_2', - ]; - $oauthToken->setParams($params); - $this->assertEquals($params, $oauthToken->getParams(), 'Unable to setup params!'); - - $newParamName = 'new_param_name'; - $newParamValue = 'new_param_value'; - $oauthToken->setParam($newParamName, $newParamValue); - $this->assertEquals($newParamValue, $oauthToken->getParam($newParamName), 'Unable to setup param by name!'); - } - - /** - * @depends testSetupParams - */ - public function testSetupParamsShortcuts() - { - $oauthToken = new Token(); - - $token = 'test_token_value'; - $oauthToken->setToken($token); - $this->assertEquals($token, $oauthToken->getToken(), 'Unable to setup token!'); - - $tokenSecret = 'test_token_secret'; - $oauthToken->setTokenSecret($tokenSecret); - $this->assertEquals($tokenSecret, $oauthToken->getTokenSecret(), 'Unable to setup token secret!'); - - $tokenExpireDuration = rand(1000, 2000); - $oauthToken->setExpireDuration($tokenExpireDuration); - $this->assertEquals($tokenExpireDuration, $oauthToken->getExpireDuration(), 'Unable to setup expire duration!'); - } - - /** - * Data provider for {@link testAutoFetchExpireDuration}. - * @return array test data. - */ - public function autoFetchExpireDurationDataProvider() - { - return [ - [ - ['expire_in' => 123345], - 123345 - ], - [ - ['expire' => 233456], - 233456 - ], - [ - ['expiry_in' => 34567], - 34567 - ], - [ - ['expiry' => 45678], - 45678 - ], - ]; - } - - /** - * @depends testSetupParamsShortcuts - * @dataProvider autoFetchExpireDurationDataProvider - * - * @param array $params - * @param $expectedExpireDuration - */ - public function testAutoFetchExpireDuration(array $params, $expectedExpireDuration) - { - $oauthToken = new Token(); - $oauthToken->setParams($params); - $this->assertEquals($expectedExpireDuration, $oauthToken->getExpireDuration()); - } - - /** - * @depends testSetupParamsShortcuts - */ - public function testGetIsExpired() - { - $oauthToken = new Token(); - $expireDuration = 3600; - $oauthToken->setExpireDuration($expireDuration); - - $this->assertFalse($oauthToken->getIsExpired(), 'Not expired token check fails!'); - - $oauthToken->createTimestamp = $oauthToken->createTimestamp - ($expireDuration +1); - $this->assertTrue($oauthToken->getIsExpired(), 'Expired token check fails!'); - } - - /** - * @depends testGetIsExpired - */ - public function testGetIsValid() - { - $oauthToken = new Token(); - $expireDuration = 3600; - $oauthToken->setExpireDuration($expireDuration); - - $this->assertFalse($oauthToken->getIsValid(), 'Empty token is valid!'); - - $oauthToken->setToken('test_token'); - $this->assertTrue($oauthToken->getIsValid(), 'Filled up token is invalid!'); - - $oauthToken->createTimestamp = $oauthToken->createTimestamp - ($expireDuration +1); - $this->assertFalse($oauthToken->getIsValid(), 'Expired token is valid!'); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/oauth/signature/BaseMethodTest.php b/tests/unit/extensions/authclient/oauth/signature/BaseMethodTest.php deleted file mode 100644 index e6f5b33..0000000 --- a/tests/unit/extensions/authclient/oauth/signature/BaseMethodTest.php +++ /dev/null @@ -1,50 +0,0 @@ -getMock('\yii\authclient\oauth\signature\BaseMethod', ['getName', 'generateSignature']); - $signatureMethod->expects($this->any())->method('getName')->will($this->returnValue('testMethodName')); - $signatureMethod->expects($this->any())->method('generateSignature')->will($this->returnValue('testSignature')); - return $signatureMethod; - } - - // Tests : - - public function testGenerateSignature() - { - $signatureMethod = $this->createTestSignatureMethod(); - - $baseString = 'test_base_string'; - $key = 'test_key'; - - $signature = $signatureMethod->generateSignature($baseString, $key); - - $this->assertNotEmpty($signature, 'Unable to generate signature!'); - } - - /** - * @depends testGenerateSignature - */ - public function testVerify() - { - $signatureMethod = $this->createTestSignatureMethod(); - - $baseString = 'test_base_string'; - $key = 'test_key'; - $signature = 'unsigned'; - $this->assertFalse($signatureMethod->verify($signature, $baseString, $key), 'Unsigned signature is valid!'); - - $generatedSignature = $signatureMethod->generateSignature($baseString, $key); - $this->assertTrue($signatureMethod->verify($generatedSignature, $baseString, $key), 'Generated signature is invalid!'); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/oauth/signature/HmacSha1Test.php b/tests/unit/extensions/authclient/oauth/signature/HmacSha1Test.php deleted file mode 100644 index 409c885..0000000 --- a/tests/unit/extensions/authclient/oauth/signature/HmacSha1Test.php +++ /dev/null @@ -1,20 +0,0 @@ -generateSignature($baseString, $key); - $this->assertNotEmpty($signature, 'Unable to generate signature!'); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/oauth/signature/PlainTextTest.php b/tests/unit/extensions/authclient/oauth/signature/PlainTextTest.php deleted file mode 100644 index 25f9abc..0000000 --- a/tests/unit/extensions/authclient/oauth/signature/PlainTextTest.php +++ /dev/null @@ -1,20 +0,0 @@ -generateSignature($baseString, $key); - $this->assertNotEmpty($signature, 'Unable to generate signature!'); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/oauth/signature/RsaSha1Test.php b/tests/unit/extensions/authclient/oauth/signature/RsaSha1Test.php deleted file mode 100644 index e4a69dc..0000000 --- a/tests/unit/extensions/authclient/oauth/signature/RsaSha1Test.php +++ /dev/null @@ -1,110 +0,0 @@ -setPrivateCertificate($this->getTestPrivateCertificate()); - $signatureMethod->setPublicCertificate($this->getTestPublicCertificate()); - - $baseString = 'test_base_string'; - $key = 'test_key'; - - $signature = $signatureMethod->generateSignature($baseString, $key); - $this->assertNotEmpty($signature, 'Unable to generate signature!'); - } - - /** - * @depends testGenerateSignature - */ - public function testVerify() - { - $signatureMethod = new RsaSha1(); - $signatureMethod->setPrivateCertificate($this->getTestPrivateCertificate()); - $signatureMethod->setPublicCertificate($this->getTestPublicCertificate()); - - $baseString = 'test_base_string'; - $key = 'test_key'; - $signature = 'unsigned'; - $this->assertFalse($signatureMethod->verify($signature, $baseString, $key), 'Unsigned signature is valid!'); - - $generatedSignature = $signatureMethod->generateSignature($baseString, $key); - $this->assertTrue($signatureMethod->verify($generatedSignature, $baseString, $key), 'Generated signature is invalid!'); - } - - public function testInitPrivateCertificate() - { - $signatureMethod = new RsaSha1(); - - $certificateFileName = __FILE__; - $signatureMethod->privateCertificateFile = $certificateFileName; - $this->assertEquals(file_get_contents($certificateFileName), $signatureMethod->getPrivateCertificate(), 'Unable to fetch private certificate from file!'); - } - - public function testInitPublicCertificate() - { - $signatureMethod = new RsaSha1(); - - $certificateFileName = __FILE__; - $signatureMethod->publicCertificateFile = $certificateFileName; - $this->assertEquals(file_get_contents($certificateFileName), $signatureMethod->getPublicCertificate(), 'Unable to fetch public certificate from file!'); - } -} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/signature/BaseMethodTest.php b/tests/unit/extensions/authclient/signature/BaseMethodTest.php new file mode 100644 index 0000000..bb91e66 --- /dev/null +++ b/tests/unit/extensions/authclient/signature/BaseMethodTest.php @@ -0,0 +1,50 @@ +getMock('\yii\authclient\signature\BaseMethod', ['getName', 'generateSignature']); + $signatureMethod->expects($this->any())->method('getName')->will($this->returnValue('testMethodName')); + $signatureMethod->expects($this->any())->method('generateSignature')->will($this->returnValue('testSignature')); + return $signatureMethod; + } + + // Tests : + + public function testGenerateSignature() + { + $signatureMethod = $this->createTestSignatureMethod(); + + $baseString = 'test_base_string'; + $key = 'test_key'; + + $signature = $signatureMethod->generateSignature($baseString, $key); + + $this->assertNotEmpty($signature, 'Unable to generate signature!'); + } + + /** + * @depends testGenerateSignature + */ + public function testVerify() + { + $signatureMethod = $this->createTestSignatureMethod(); + + $baseString = 'test_base_string'; + $key = 'test_key'; + $signature = 'unsigned'; + $this->assertFalse($signatureMethod->verify($signature, $baseString, $key), 'Unsigned signature is valid!'); + + $generatedSignature = $signatureMethod->generateSignature($baseString, $key); + $this->assertTrue($signatureMethod->verify($generatedSignature, $baseString, $key), 'Generated signature is invalid!'); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/signature/HmacSha1Test.php b/tests/unit/extensions/authclient/signature/HmacSha1Test.php new file mode 100644 index 0000000..6305e03 --- /dev/null +++ b/tests/unit/extensions/authclient/signature/HmacSha1Test.php @@ -0,0 +1,20 @@ +generateSignature($baseString, $key); + $this->assertNotEmpty($signature, 'Unable to generate signature!'); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/signature/PlainTextTest.php b/tests/unit/extensions/authclient/signature/PlainTextTest.php new file mode 100644 index 0000000..c5199a5 --- /dev/null +++ b/tests/unit/extensions/authclient/signature/PlainTextTest.php @@ -0,0 +1,20 @@ +generateSignature($baseString, $key); + $this->assertNotEmpty($signature, 'Unable to generate signature!'); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/signature/RsaSha1Test.php b/tests/unit/extensions/authclient/signature/RsaSha1Test.php new file mode 100644 index 0000000..0848ade --- /dev/null +++ b/tests/unit/extensions/authclient/signature/RsaSha1Test.php @@ -0,0 +1,110 @@ +setPrivateCertificate($this->getTestPrivateCertificate()); + $signatureMethod->setPublicCertificate($this->getTestPublicCertificate()); + + $baseString = 'test_base_string'; + $key = 'test_key'; + + $signature = $signatureMethod->generateSignature($baseString, $key); + $this->assertNotEmpty($signature, 'Unable to generate signature!'); + } + + /** + * @depends testGenerateSignature + */ + public function testVerify() + { + $signatureMethod = new RsaSha1(); + $signatureMethod->setPrivateCertificate($this->getTestPrivateCertificate()); + $signatureMethod->setPublicCertificate($this->getTestPublicCertificate()); + + $baseString = 'test_base_string'; + $key = 'test_key'; + $signature = 'unsigned'; + $this->assertFalse($signatureMethod->verify($signature, $baseString, $key), 'Unsigned signature is valid!'); + + $generatedSignature = $signatureMethod->generateSignature($baseString, $key); + $this->assertTrue($signatureMethod->verify($generatedSignature, $baseString, $key), 'Generated signature is invalid!'); + } + + public function testInitPrivateCertificate() + { + $signatureMethod = new RsaSha1(); + + $certificateFileName = __FILE__; + $signatureMethod->privateCertificateFile = $certificateFileName; + $this->assertEquals(file_get_contents($certificateFileName), $signatureMethod->getPrivateCertificate(), 'Unable to fetch private certificate from file!'); + } + + public function testInitPublicCertificate() + { + $signatureMethod = new RsaSha1(); + + $certificateFileName = __FILE__; + $signatureMethod->publicCertificateFile = $certificateFileName; + $this->assertEquals(file_get_contents($certificateFileName), $signatureMethod->getPublicCertificate(), 'Unable to fetch public certificate from file!'); + } +} \ No newline at end of file