diff --git a/extensions/yii/authclient/LICENSE.md b/extensions/yii/authclient/LICENSE.md new file mode 100644 index 0000000..0bb1a8d --- /dev/null +++ b/extensions/yii/authclient/LICENSE.md @@ -0,0 +1,32 @@ +The Yii framework is free software. It is released under the terms of +the following BSD License. + +Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Yii Software LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/extensions/yii/authclient/README.md b/extensions/yii/authclient/README.md new file mode 100644 index 0000000..b4c57ae --- /dev/null +++ b/extensions/yii/authclient/README.md @@ -0,0 +1,30 @@ +AuthClient Extension for Yii 2 +============================== + +This extension adds [OpenID](http://openid.net/), [OAuth](http://oauth.net/) and [OAuth2](http://oauth.net/2/) consumers for the Yii 2 framework. + + +Installation +------------ + +The preferred way to install this extension is through [composer](http://getcomposer.org/download/). + +Either run + +``` +php composer.phar require yiisoft/yii2-authclient "*" +``` + +or add + +```json +"yiisoft/yii2-authclient": "*" +``` + +to the require section of your composer.json. + + +Usage & Documentation +--------------------- + +This extension... \ No newline at end of file diff --git a/extensions/yii/authclient/composer.json b/extensions/yii/authclient/composer.json new file mode 100644 index 0000000..d40889e --- /dev/null +++ b/extensions/yii/authclient/composer.json @@ -0,0 +1,28 @@ +{ + "name": "yiisoft/yii2-authclient", + "description": "External authentication via OAuth and OpenID for the Yii framework", + "keywords": ["yii", "OAuth", "OpenID", "auth"], + "type": "yii2-extension", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/yiisoft/yii2/issues?state=open", + "forum": "http://www.yiiframework.com/forum/", + "wiki": "http://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii", + "source": "https://github.com/yiisoft/yii2" + }, + "authors": [ + { + "name": "Paul Klimov", + "email": "klimov.paul@gmail.com" + } + ], + "require": { + "yiisoft/yii2": "*", + "ext-curl": "*" + }, + "autoload": { + "psr-0": { "yii\\authclient\\": "" } + }, + "target-dir": "yii/authclient" +} diff --git a/extensions/yii/authclient/oauth/BaseClient.php b/extensions/yii/authclient/oauth/BaseClient.php new file mode 100644 index 0000000..1be63cc --- /dev/null +++ b/extensions/yii/authclient/oauth/BaseClient.php @@ -0,0 +1,504 @@ + + * @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 new file mode 100644 index 0000000..5165879 --- /dev/null +++ b/extensions/yii/authclient/oauth/Client1.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 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 new file mode 100644 index 0000000..128fad5 --- /dev/null +++ b/extensions/yii/authclient/oauth/Client2.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 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 new file mode 100644 index 0000000..16e6d1e --- /dev/null +++ b/extensions/yii/authclient/oauth/Token.php @@ -0,0 +1,186 @@ + + * @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 new file mode 100644 index 0000000..1fcefdf --- /dev/null +++ b/extensions/yii/authclient/oauth/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/oauth/signature/HmacSha1.php b/extensions/yii/authclient/oauth/signature/HmacSha1.php new file mode 100644 index 0000000..3a7f223 --- /dev/null +++ b/extensions/yii/authclient/oauth/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/oauth/signature/PlainText.php b/extensions/yii/authclient/oauth/signature/PlainText.php new file mode 100644 index 0000000..1140814 --- /dev/null +++ b/extensions/yii/authclient/oauth/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/oauth/signature/RsaSha1.php b/extensions/yii/authclient/oauth/signature/RsaSha1.php new file mode 100644 index 0000000..e140fd5 --- /dev/null +++ b/extensions/yii/authclient/oauth/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/extensions/yii/authclient/openid/Client.php b/extensions/yii/authclient/openid/Client.php new file mode 100644 index 0000000..709259d --- /dev/null +++ b/extensions/yii/authclient/openid/Client.php @@ -0,0 +1,23 @@ + + * @since 2.0 + */ +class Client extends Component +{ + +} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/TestCase.php b/tests/unit/extensions/authclient/TestCase.php new file mode 100644 index 0000000..30fe5de --- /dev/null +++ b/tests/unit/extensions/authclient/TestCase.php @@ -0,0 +1,33 @@ +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 new file mode 100644 index 0000000..e882a68 --- /dev/null +++ b/tests/unit/extensions/authclient/oauth/Client1Test.php @@ -0,0 +1,109 @@ +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 new file mode 100644 index 0000000..c8eeab8 --- /dev/null +++ b/tests/unit/extensions/authclient/oauth/Client2Test.php @@ -0,0 +1,33 @@ +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 new file mode 100644 index 0000000..d319198 --- /dev/null +++ b/tests/unit/extensions/authclient/oauth/TokenTest.php @@ -0,0 +1,133 @@ + '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 new file mode 100644 index 0000000..e6f5b33 --- /dev/null +++ b/tests/unit/extensions/authclient/oauth/signature/BaseMethodTest.php @@ -0,0 +1,50 @@ +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 new file mode 100644 index 0000000..409c885 --- /dev/null +++ b/tests/unit/extensions/authclient/oauth/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/oauth/signature/PlainTextTest.php b/tests/unit/extensions/authclient/oauth/signature/PlainTextTest.php new file mode 100644 index 0000000..25f9abc --- /dev/null +++ b/tests/unit/extensions/authclient/oauth/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/oauth/signature/RsaSha1Test.php b/tests/unit/extensions/authclient/oauth/signature/RsaSha1Test.php new file mode 100644 index 0000000..e4a69dc --- /dev/null +++ b/tests/unit/extensions/authclient/oauth/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