From cdae9b60463d2ef9bfaf4312e5f158caa2fe5069 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Tue, 10 Dec 2013 14:49:51 +0200 Subject: [PATCH 01/37] Extension "authclient" added as draft. --- extensions/yii/authclient/LICENSE.md | 32 ++ extensions/yii/authclient/README.md | 30 ++ extensions/yii/authclient/composer.json | 28 ++ 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 | 23 + tests/unit/extensions/authclient/TestCase.php | 33 ++ .../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 +++++ 21 files changed, 2398 insertions(+) create mode 100644 extensions/yii/authclient/LICENSE.md create mode 100644 extensions/yii/authclient/README.md create mode 100644 extensions/yii/authclient/composer.json create mode 100644 extensions/yii/authclient/oauth/BaseClient.php create mode 100644 extensions/yii/authclient/oauth/Client1.php create mode 100644 extensions/yii/authclient/oauth/Client2.php create mode 100644 extensions/yii/authclient/oauth/Token.php create mode 100644 extensions/yii/authclient/oauth/signature/BaseMethod.php create mode 100644 extensions/yii/authclient/oauth/signature/HmacSha1.php create mode 100644 extensions/yii/authclient/oauth/signature/PlainText.php create mode 100644 extensions/yii/authclient/oauth/signature/RsaSha1.php create mode 100644 extensions/yii/authclient/openid/Client.php create mode 100644 tests/unit/extensions/authclient/TestCase.php create mode 100644 tests/unit/extensions/authclient/oauth/BaseClientTest.php create mode 100644 tests/unit/extensions/authclient/oauth/Client1Test.php create mode 100644 tests/unit/extensions/authclient/oauth/Client2Test.php create mode 100644 tests/unit/extensions/authclient/oauth/TokenTest.php create mode 100644 tests/unit/extensions/authclient/oauth/signature/BaseMethodTest.php create mode 100644 tests/unit/extensions/authclient/oauth/signature/HmacSha1Test.php create mode 100644 tests/unit/extensions/authclient/oauth/signature/PlainTextTest.php create mode 100644 tests/unit/extensions/authclient/oauth/signature/RsaSha1Test.php 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 From 82682598768948b06bcaa9f33f3598995d3bd54b Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Tue, 10 Dec 2013 17:28:39 +0200 Subject: [PATCH 02/37] OpenID client created as draft. --- extensions/yii/authclient/openid/Client.php | 757 ++++++++++++++++++++++++++++ 1 file changed, 757 insertions(+) diff --git a/extensions/yii/authclient/openid/Client.php b/extensions/yii/authclient/openid/Client.php index 709259d..bd06988 100644 --- a/extensions/yii/authclient/openid/Client.php +++ b/extensions/yii/authclient/openid/Client.php @@ -8,16 +8,773 @@ namespace yii\authclient\openid; use yii\base\Component; +use yii\base\Exception; +use yii\base\NotSupportedException; /** * Class Client * * @see http://openid.net/ * + * @property string $returnUrl ??? + * @property mixed $identity ??? + * @property string $trustRoot ??? + * @property string $realm alias of [[trustRoot]]. + * @property mixed $mode ??? This property is read-only. + * * @author Paul Klimov * @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 From 41eaa2df07cb902814b91d59fce742ccdc57af76 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Wed, 11 Dec 2013 13:56:14 +0200 Subject: [PATCH 03/37] 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 From 6171287f79b1a4757d50f13df63ac1e54e62eee6 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Wed, 11 Dec 2013 17:17:35 +0200 Subject: [PATCH 04/37] Classes "\yii\authclient\provider\*" created as draft. --- extensions/yii/authclient/provider/OAuth1.php | 51 ++++++++++++ extensions/yii/authclient/provider/OAuth2.php | 58 ++++++++++++++ extensions/yii/authclient/provider/OpenId.php | 54 +++++++++++-- .../yii/authclient/provider/ProviderInterface.php | 2 +- .../yii/authclient/provider/ProviderTrait.php | 45 +++++++++++ .../yii/authclient/provider/views/redirect.php | 38 +++++++++ tests/unit/extensions/authclient/TestCase.php | 11 +-- .../authclient/provider/ProviderTraitTest.php | 90 ++++++++++++++++++++++ 8 files changed, 336 insertions(+), 13 deletions(-) create mode 100644 extensions/yii/authclient/provider/OAuth1.php create mode 100644 extensions/yii/authclient/provider/OAuth2.php create mode 100644 extensions/yii/authclient/provider/views/redirect.php create mode 100644 tests/unit/extensions/authclient/provider/ProviderTraitTest.php diff --git a/extensions/yii/authclient/provider/OAuth1.php b/extensions/yii/authclient/provider/OAuth1.php new file mode 100644 index 0000000..4220c39 --- /dev/null +++ b/extensions/yii/authclient/provider/OAuth1.php @@ -0,0 +1,51 @@ + + * @since 2.0 + */ +class OAuth1 extends \yii\authclient\OAuth1 implements ProviderInterface +{ + use ProviderTrait; + + /** + * @inheritdoc + */ + public function authenticate() + { + // user denied error + if (isset($_GET['denied'])) { + return $this->redirectCancel(); + } + + if (isset($_REQUEST['oauth_token'])) { + $oauthToken = $_REQUEST['oauth_token']; + } + + if (!isset($oauthToken)) { + // Get request token. + $requestToken = $this->fetchRequestToken(); + // Get authorization URL. + $url = $this->buildAuthUrl($requestToken); + // Redirect to authorization URL. + return Yii::$app->getResponse()->redirect($url); + } else { + // Upgrade to access token. + $accessToken = $this->fetchAccessToken(); + $this->isAuthenticated = true; + } + + return $this->isAuthenticated; + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/provider/OAuth2.php b/extensions/yii/authclient/provider/OAuth2.php new file mode 100644 index 0000000..e55328e --- /dev/null +++ b/extensions/yii/authclient/provider/OAuth2.php @@ -0,0 +1,58 @@ + + * @since 2.0 + */ +class OAuth2 extends \yii\authclient\OAuth2 implements ProviderInterface +{ + use ProviderTrait; + + /** + * @inheritdoc + */ + public function authenticate() + { + if (isset($_GET['error'])) { + if ($_GET['error'] == 'access_denied') { + // user denied error + return $this->redirectCancel(); + } else { + // request error + if (isset($_GET['error_description'])) { + $errorMessage = $_GET['error_description']; + } elseif (isset($_GET['error_message'])) { + $errorMessage = $_GET['error_message']; + } else { + $errorMessage = http_build_query($_GET); + } + throw new Exception('Auth error: ' . $errorMessage); + } + } + + // Get the access_token and save them to the session. + if (isset($_GET['code'])) { + $code = $_GET['code']; + $token = $this->fetchAccessToken($code); + if (!empty($token)) { + $this->isAuthenticated = true; + } + } else { + $url = $this->buildAuthUrl(); + return Yii::$app->getResponse()->redirect($url); + } + + return $this->isAuthenticated; + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/provider/OpenId.php b/extensions/yii/authclient/provider/OpenId.php index ad00a39..ff1985c 100644 --- a/extensions/yii/authclient/provider/OpenId.php +++ b/extensions/yii/authclient/provider/OpenId.php @@ -7,7 +7,9 @@ namespace yii\authclient\provider; -use yii\authclient\openid\Client; +use Yii; +use yii\base\Exception; +use yii\web\HttpException; /** * Class OpenId @@ -15,16 +17,58 @@ use yii\authclient\openid\Client; * @author Paul Klimov * @since 2.0 */ -class OpenId extends Client implements ProviderInterface +class OpenId extends \yii\authclient\OpenId implements ProviderInterface { use ProviderTrait; /** - * Authenticate the user. - * @return boolean whether user was successfully authenticated. + * @inheritdoc */ public function authenticate() { - // TODO: Implement authenticate() method. + if (!empty($_REQUEST['openid_mode'])) { + switch ($_REQUEST['openid_mode']) { + case 'id_res': + if ($this->validate()) { + $attributes = array( + 'id' => $this->identity + ); + $rawAttributes = $this->getAttributes(); + foreach ($this->getRequiredAttributes() as $openIdAttributeName) { + if (isset($rawAttributes[$openIdAttributeName])) { + $attributes[$openIdAttributeName] = $rawAttributes[$openIdAttributeName]; + } else { + throw new Exception('Unable to complete the authentication because the required data was not received.'); + } + } + $this->setAttributes($attributes); + $this->isAuthenticated = true; + return true; + } else { + throw new Exception('Unable to complete the authentication because the required data was not received.'); + } + break; + case 'cancel': + $this->redirectCancel(); + break; + default: + throw new HttpException(400); + break; + } + } else { + $this->identity = $this->authUrl; // Setting identifier + $this->required = []; // Try to get info from openid provider + foreach ($this->getRequiredAttributes() as $openIdAttributeName) { + $this->required[] = $openIdAttributeName; + } + $request = Yii::$app->getRequest(); + $this->realm = $request->getHostInfo(); + $this->returnUrl = $this->realm . $request->getUrl(); // getting return URL + + $url = $this->authUrl(); + return Yii::$app->getResponse()->redirect($url); + } + + return false; } } \ No newline at end of file diff --git a/extensions/yii/authclient/provider/ProviderInterface.php b/extensions/yii/authclient/provider/ProviderInterface.php index 9be8a2f..b3f2b27 100644 --- a/extensions/yii/authclient/provider/ProviderInterface.php +++ b/extensions/yii/authclient/provider/ProviderInterface.php @@ -67,7 +67,7 @@ interface ProviderInterface /** * Authenticate the user. - * @return boolean whether user was successfully authenticated. + * @return \yii\web\Response|boolean response instance or 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 index 5926302..129fa0d 100644 --- a/extensions/yii/authclient/provider/ProviderTrait.php +++ b/extensions/yii/authclient/provider/ProviderTrait.php @@ -173,4 +173,49 @@ trait ProviderTrait { return Yii::$app->getRequest()->getAbsoluteUrl(); } + + /** + * Redirect to the given URL or simply close the popup window. + * @param mixed $url URL to redirect, could be a string or array config to generate a valid URL. + * @param boolean $enforceRedirect indicates if redirect should be performed even in case of popup window. + * @return \yii\web\Response response instance. + */ + public function redirect($url, $enforceRedirect = true) + { + $viewData = [ + 'url' => $url, + 'enforceRedirect' => $enforceRedirect, + ]; + $viewFile = __DIR__ . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'redirect.php'; + + $response = Yii::$app->getResponse(); + $response->content = Yii::$app->getView()->renderFile($viewFile, $viewData); + return $response; + } + + /** + * Redirect to the URL. If URL is null, {@link successUrl} will be used. + * @param string $url URL to redirect. + * @return \yii\web\Response response instance. + */ + public function redirectSuccess($url = null) + { + if ($url === null) { + $url = $this->getSuccessUrl(); + } + return $this->redirect($url); + } + + /** + * Redirect to the {@link cancelUrl} or simply close the popup window. + * @param string $url URL to redirect. + * @return \yii\web\Response response instance. + */ + public function redirectCancel($url = null) + { + if ($url === null) { + $url = $this->getCancelUrl(); + } + return $this->redirect($url, false); + } } \ No newline at end of file diff --git a/extensions/yii/authclient/provider/views/redirect.php b/extensions/yii/authclient/provider/views/redirect.php new file mode 100644 index 0000000..e85aa46 --- /dev/null +++ b/extensions/yii/authclient/provider/views/redirect.php @@ -0,0 +1,38 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/unit/extensions/authclient/TestCase.php b/tests/unit/extensions/authclient/TestCase.php index 30fe5de..3a033b4 100644 --- a/tests/unit/extensions/authclient/TestCase.php +++ b/tests/unit/extensions/authclient/TestCase.php @@ -10,16 +10,11 @@ use Yii; */ class TestCase extends \yiiunit\TestCase { - public static function setUpBeforeClass() - { - static::loadClassMap(); - } - /** * Adds sphinx extension files to [[Yii::$classPath]], * avoiding the necessity of usage Composer autoloader. */ - protected static function loadClassMap() + public static function loadClassMap() { $baseNameSpace = 'yii/authclient'; $basePath = realpath(__DIR__. '/../../../../extensions/yii/authclient'); @@ -30,4 +25,6 @@ class TestCase extends \yiiunit\TestCase Yii::$classMap[$classFullName] = $file; } } -} \ No newline at end of file +} + +TestCase::loadClassMap(); \ No newline at end of file diff --git a/tests/unit/extensions/authclient/provider/ProviderTraitTest.php b/tests/unit/extensions/authclient/provider/ProviderTraitTest.php new file mode 100644 index 0000000..8289eb3 --- /dev/null +++ b/tests/unit/extensions/authclient/provider/ProviderTraitTest.php @@ -0,0 +1,90 @@ + [ + 'user' => [ + 'identityClass' => '\yii\web\IdentityInterface' + ], + 'request' => [ + 'hostInfo' => 'http://testdomain.com', + 'scriptUrl' => '/index.php', + ], + ] + ]; + $this->mockApplication($config, '\yii\web\Application'); + } + + public function testSetGet() + { + $provider = new Provider(); + + $id = 'test_service_id'; + $provider->setId($id); + $this->assertEquals($id, $provider->getId(), 'Unable to setup id!'); + + $successUrl = 'http://test.success.url'; + $provider->setSuccessUrl($successUrl); + $this->assertEquals($successUrl, $provider->getSuccessUrl(), 'Unable to setup success URL!'); + + $cancelUrl = 'http://test.cancel.url'; + $provider->setCancelUrl($cancelUrl); + $this->assertEquals($cancelUrl, $provider->getCancelUrl(), 'Unable to setup cancel URL!'); + } + + public function testGetDescriptiveData() + { + $provider = new Provider(); + + $this->assertNotEmpty($provider->getName(), 'Unable to get name!'); + $this->assertNotEmpty($provider->getTitle(), 'Unable to get title!'); + } + + /** + * @depends testSetGet + */ + public function testGetDefaultSuccessUrl() + { + $provider = new Provider(); + + $this->assertNotEmpty($provider->getSuccessUrl(), 'Unable to get default success URL!'); + } + + /** + * @depends testSetGet + */ + public function testGetDefaultCancelUrl() + { + $provider = new Provider(); + + $this->assertNotEmpty($provider->getSuccessUrl(), 'Unable to get default cancel URL!'); + } + + public function testRedirect() + { + $provider = new Provider(); + + $url = 'http://test.url'; + $response = $provider->redirect($url, true); + + $this->assertContains($url, $response->content); + } +} + +class Provider extends Object implements ProviderInterface +{ + use ProviderTrait; + + public function authenticate() {} +} \ No newline at end of file From 691cc14c1c7def5c6b439e3bd7c50d3632f0731c Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Thu, 12 Dec 2013 12:46:17 +0200 Subject: [PATCH 05/37] "\yii\authclient\provider\Collection" added. --- extensions/yii/authclient/provider/Collection.php | 105 +++++++++++++++++++++ .../authclient/provider/CollectionTest.php | 90 ++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100644 extensions/yii/authclient/provider/Collection.php create mode 100644 tests/unit/extensions/authclient/provider/CollectionTest.php diff --git a/extensions/yii/authclient/provider/Collection.php b/extensions/yii/authclient/provider/Collection.php new file mode 100644 index 0000000..303f109 --- /dev/null +++ b/extensions/yii/authclient/provider/Collection.php @@ -0,0 +1,105 @@ + [ + * 'auth' => [ + * 'class' => 'yii\authclient\provider\Collection', + * 'providers' => [ + * 'google' => [ + * 'class' => 'yii\authclient\provider\GoogleOpenId' + * ], + * 'facebook' => [ + * 'class' => 'yii\authclient\provider\Facebook', + * 'clientId' => 'facebook_client_id', + * 'clientSecret' => 'facebook_client_secret', + * ], + * ], + * ] + * ... + * ] + * ~~~ + * + * @author Paul Klimov + * @since 2.0 + */ +class Collection extends Component +{ + /** + * @var array list of Auth providers with their configuration in format: 'providerId' => [...] + */ + private $_providers = []; + + /** + * @param array $providers list of auth providers + */ + public function setProviders(array $providers) + { + $this->_providers = $providers; + } + + /** + * @return ProviderInterface[] list of auth providers. + */ + public function getProviders() + { + $providers = []; + foreach ($this->_providers as $id => $provider) { + $providers[$id] = $this->getProvider($id); + } + return $providers; + } + + /** + * @param string $id service id. + * @return ProviderInterface auth service instance. + * @throws InvalidParamException on non existing provider request. + */ + public function getProvider($id) + { + if (!array_key_exists($id, $this->_providers)) { + throw new InvalidParamException("Unknown auth provider '{$id}'."); + } + if (!is_object($this->_providers[$id])) { + $this->_providers[$id] = $this->createProvider($id, $this->_providers[$id]); + } + return $this->_providers[$id]; + } + + /** + * Checks if provider exists in the hub. + * @param string $id provider id. + * @return boolean whether provider exist. + */ + public function hasProvider($id) + { + return array_key_exists($id, $this->_providers); + } + + /** + * Creates auth provider instance from its array configuration. + * @param string $id auth provider id. + * @param array $config auth provider instance configuration. + * @return ProviderInterface auth provider instance. + */ + protected function createProvider($id, array $config) + { + $config['id'] = $id; + return Yii::createObject($config); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/provider/CollectionTest.php b/tests/unit/extensions/authclient/provider/CollectionTest.php new file mode 100644 index 0000000..9c65e59 --- /dev/null +++ b/tests/unit/extensions/authclient/provider/CollectionTest.php @@ -0,0 +1,90 @@ + new TestProvider(), + 'testProvider2' => new TestProvider(), + ]; + $collection->setProviders($providers); + $this->assertEquals($providers, $collection->getProviders(), 'Unable to setup providers!'); + } + + /** + * @depends testSetGet + */ + public function testGetProviderById() + { + $collection = new Collection(); + + $providerId = 'testProviderId'; + $provider = new TestProvider(); + $providers = [ + $providerId => $provider + ]; + $collection->setProviders($providers); + + $this->assertEquals($provider, $collection->getProvider($providerId), 'Unable to get provider by id!'); + } + + /** + * @depends testGetProviderById + */ + public function testCreateProvider() + { + $collection = new Collection(); + + $providerId = 'testProviderId'; + $providerClassName = TestProvider::className(); + $providers = [ + $providerId => [ + 'class' => $providerClassName + ] + ]; + $collection->setProviders($providers); + + $provider = $collection->getProvider($providerId); + $this->assertTrue(is_object($provider), 'Unable to create provider by config!'); + $this->assertTrue(is_a($provider, $providerClassName), 'Provider has wrong class name!'); + } + + /** + * @depends testSetGet + */ + public function testHasProvider() + { + $collection = new Collection(); + + $providerName = 'testProviderName'; + $providers = [ + $providerName => [ + 'class' => 'TestProvider1' + ], + ]; + $collection->setProviders($providers); + + $this->assertTrue($collection->hasProvider($providerName), 'Existing provider check fails!'); + $this->assertFalse($collection->hasProvider('unExistingProviderName'), 'Not existing provider check fails!'); + } +} + +class TestProvider extends Object implements ProviderInterface +{ + use ProviderTrait; + + public function authenticate() {} +} \ No newline at end of file From 63b16fb430c14f4df5c1258cefbfc96f401af30a Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Thu, 12 Dec 2013 16:13:48 +0200 Subject: [PATCH 06/37] "\yii\authclient\widgets\Choice" added. --- extensions/yii/authclient/provider/Collection.php | 2 +- extensions/yii/authclient/widgets/Choice.php | 198 +++++++++++++++++++++ extensions/yii/authclient/widgets/ChoiceAsset.php | 27 +++ .../yii/authclient/widgets/assets/authchoice.js | 53 ++++++ 4 files changed, 279 insertions(+), 1 deletion(-) create mode 100644 extensions/yii/authclient/widgets/Choice.php create mode 100644 extensions/yii/authclient/widgets/ChoiceAsset.php create mode 100644 extensions/yii/authclient/widgets/assets/authchoice.js diff --git a/extensions/yii/authclient/provider/Collection.php b/extensions/yii/authclient/provider/Collection.php index 303f109..309eae5 100644 --- a/extensions/yii/authclient/provider/Collection.php +++ b/extensions/yii/authclient/provider/Collection.php @@ -97,7 +97,7 @@ class Collection extends Component * @param array $config auth provider instance configuration. * @return ProviderInterface auth provider instance. */ - protected function createProvider($id, array $config) + protected function createProvider($id, $config) { $config['id'] = $id; return Yii::createObject($config); diff --git a/extensions/yii/authclient/widgets/Choice.php b/extensions/yii/authclient/widgets/Choice.php new file mode 100644 index 0000000..5585529 --- /dev/null +++ b/extensions/yii/authclient/widgets/Choice.php @@ -0,0 +1,198 @@ + + * @since 2.0 + */ +class Choice extends Widget +{ + /** + * @var ProviderInterface[] auth providers list. + */ + protected $_providers; + /** + * @var string name of the auth provider collection application component. + * This component will be used to fetch {@link services} value if it is not set. + */ + public $providerCollection; + /** + * @var array configuration for the external services base authentication URL. + */ + protected $_baseAuthUrl; + /** + * @var string name of the GET param , which should be used to passed auth provider id to URL + * defined by {@link baseAuthUrl}. + */ + public $providerIdGetParamName = 'provider'; + /** + * @var array the HTML attributes that should be rendered in the div HTML tag representing the container element. + */ + public $mainContainerHtmlOptions = [ + 'class' => 'services' + ]; + /** + * @var boolean indicates if popup window should be used instead of direct links. + */ + public $popupMode = true; + /** + * @var boolean indicates if widget content, should be rendered automatically. + * Note: this value automatically set to 'false' at the first call of [[createProviderUrl()]] + */ + public $autoRender = true; + + /** + * @param ProviderInterface[] $providers auth providers + */ + public function setProviders(array $providers) + { + $this->_providers = $providers; + } + + /** + * @return ProviderInterface[] auth providers + */ + public function getProviders() + { + if ($this->_providers === null) { + $this->_providers = $this->defaultProviders(); + } + return $this->_providers; + } + + /** + * @param array $baseAuthUrl base auth URL configuration. + */ + public function setBaseAuthUrl(array $baseAuthUrl) + { + $this->_baseAuthUrl = $baseAuthUrl; + } + + /** + * @return array base auth URL configuration. + */ + public function getBaseAuthUrl() + { + if (!is_array($this->_baseAuthUrl)) { + $this->_baseAuthUrl = $this->defaultBaseAuthUrl(); + } + return $this->_baseAuthUrl; + } + + /** + * Returns default auth providers list. + * @return ProviderInterface[] auth providers list. + */ + protected function defaultProviders() + { + /** @var $collection \yii\authclient\provider\Collection */ + $collection = Yii::$app->getComponent($this->providerCollection); + return $collection->getProviders(); + } + + /** + * Composes default base auth URL configuration. + * @return array base auth URL configuration. + */ + protected function defaultBaseAuthUrl() + { + $baseAuthUrl = [ + Yii::$app->controller->getRoute() + ]; + $params = $_GET; + unset($params[$this->providerIdGetParamName]); + $baseAuthUrl = array_merge($baseAuthUrl, $params); + return $baseAuthUrl; + } + + /** + * Outputs external service auth link. + * @param ProviderInterface $service external auth service instance. + * @param string $text link text, if not set - default value will be generated. + * @param array $htmlOptions link HTML options. + */ + public function providerLink($service, $text = null, array $htmlOptions = []) + { + if ($text === null) { + $text = Html::tag('span', ['class' => 'auth-icon ' . $service->getName()], ''); + $text .= Html::tag('span', ['class' => 'auth-title'], $service->getTitle()); + } + if (!array_key_exists('class', $htmlOptions)) { + $htmlOptions['class'] = 'auth-link ' . $service->getName(); + } + if ($this->popupMode) { + if (isset($service->popupWidth)) { + $htmlOptions['data-popup-width'] = $service->popupWidth; + } + if (isset($service->popupHeight)) { + $htmlOptions['data-popup-height'] = $service->popupHeight; + } + } + echo Html::a($text, $this->createProviderUrl($service), $htmlOptions); + } + + /** + * Composes external service auth URL. + * @param ProviderInterface $provider external auth service instance. + * @return string auth URL. + */ + public function createProviderUrl($provider) + { + $this->autoRender = false; + $url = $this->getBaseAuthUrl(); + $url[$this->providerIdGetParamName] = $provider->getId(); + return Html::url($url); + } + + /** + * Renders the main content, which includes all external services links. + */ + protected function renderMainContent() + { + echo Html::beginTag('ul', ['class' => 'auth-services clear']); + foreach ($this->getProviders() as $externalService) { + echo Html::beginTag('li', ['class' => 'auth-service']); + $this->providerLink($externalService); + echo Html::endTag('li'); + } + echo Html::endTag('ul'); + } + + /** + * Initializes the widget. + */ + public function init() + { + if ($this->popupMode) { + $view = Yii::$app->getView(); + ChoiceAsset::register($view); + $view->registerJs("\$('#" . $this->getId() . "').authchoice();"); + } + $this->mainContainerHtmlOptions['id'] = $this->getId(); + echo Html::beginTag('div', $this->mainContainerHtmlOptions); + } + + /** + * Runs the widget. + */ + public function run() + { + if ($this->autoRender) { + $this->renderMainContent(); + } + echo Html::endTag('div'); + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/widgets/ChoiceAsset.php b/extensions/yii/authclient/widgets/ChoiceAsset.php new file mode 100644 index 0000000..c5fcbef --- /dev/null +++ b/extensions/yii/authclient/widgets/ChoiceAsset.php @@ -0,0 +1,27 @@ + + * @since 2.0 + */ +class ChoiceAsset extends AssetBundle +{ + public $sourcePath = '@yii/authclient/widgets/assets'; + public $js = [ + 'authchoice.js', + ]; + public $depends = [ + 'yii\web\YiiAsset', + ]; +} \ No newline at end of file diff --git a/extensions/yii/authclient/widgets/assets/authchoice.js b/extensions/yii/authclient/widgets/assets/authchoice.js new file mode 100644 index 0000000..b0aa885 --- /dev/null +++ b/extensions/yii/authclient/widgets/assets/authchoice.js @@ -0,0 +1,53 @@ +jQuery(function($) { + var authChoicePopup; + + $.fn.authchoice = function(options) { + options = $.extend({ + popup: { + resizable: 'yes', + scrollbars: 'no', + toolbar: 'no', + menubar: 'no', + location: 'no', + directories: 'no', + status: 'yes', + width: 450, + height: 380 + } + }, options); + + return this.each(function() { + var container = $(this); + + container.find('a').on('click', function(e) { + e.preventDefault(); + if (authChoicePopup !== undefined) { + authChoicePopup.close(); + } + var url = this.href; + var popupOptions = options.popup; + + var localPopupWidth = this.getAttribute('data-popup-width'); + if (localPopupWidth) { + popupOptions.width = localPopupWidth; + } + var localPopupHeight = this.getAttribute('data-popup-height'); + if (localPopupWidth) { + popupOptions.height = localPopupHeight; + } + + popupOptions.left = (window.screen.width - options.popup.width) / 2; + popupOptions.top = (window.screen.height - options.popup.height) / 2; + + var popupFeatureParts = []; + for (var propName in popupOptions) { + popupFeatureParts.push(propName + '=' + popupOptions[propName]); + } + var popupFeature = popupFeatureParts.join(','); + + authChoicePopup = window.open(url, 'yii_auth_choice', popupFeature); + authChoicePopup.focus(); + }); + }); + }; +}); From ac594309aa546d8189723edebcd035f179d03d56 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Thu, 12 Dec 2013 16:45:10 +0200 Subject: [PATCH 07/37] Doc comments at "\yii\authclient\widgets\Choice" fixed. --- extensions/yii/authclient/widgets/Choice.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/extensions/yii/authclient/widgets/Choice.php b/extensions/yii/authclient/widgets/Choice.php index 5585529..94b32ff 100644 --- a/extensions/yii/authclient/widgets/Choice.php +++ b/extensions/yii/authclient/widgets/Choice.php @@ -15,6 +15,9 @@ use yii\authclient\provider\ProviderInterface; /** * Class Choice * + * @property ProviderInterface[] $providers auth providers list. + * @property array $baseAuthUrl configuration for the external services base authentication URL. + * * @author Paul Klimov * @since 2.0 */ @@ -23,7 +26,7 @@ class Choice extends Widget /** * @var ProviderInterface[] auth providers list. */ - protected $_providers; + private $_providers; /** * @var string name of the auth provider collection application component. * This component will be used to fetch {@link services} value if it is not set. @@ -32,7 +35,7 @@ class Choice extends Widget /** * @var array configuration for the external services base authentication URL. */ - protected $_baseAuthUrl; + private $_baseAuthUrl; /** * @var string name of the GET param , which should be used to passed auth provider id to URL * defined by {@link baseAuthUrl}. From 1ef606da9ab9c9a6631f37cc15dfb4351c646099 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Fri, 13 Dec 2013 16:28:53 +0200 Subject: [PATCH 08/37] "yii\authclient\AuthAction" created. --- extensions/yii/authclient/AuthAction.php | 317 +++++++++++++++++++++ .../unit/extensions/authclient/AuthActionTest.php | 68 +++++ 2 files changed, 385 insertions(+) create mode 100644 extensions/yii/authclient/AuthAction.php create mode 100644 tests/unit/extensions/authclient/AuthActionTest.php diff --git a/extensions/yii/authclient/AuthAction.php b/extensions/yii/authclient/AuthAction.php new file mode 100644 index 0000000..f3dc453 --- /dev/null +++ b/extensions/yii/authclient/AuthAction.php @@ -0,0 +1,317 @@ + + * @since 2.0 + */ +class AuthAction extends Action +{ + /** + * @var string name of the auth provider collection application component. + * This component will be used to fetch {@link services} value if it is not set. + */ + public $providerCollection; + /** + * @var string name of the GET param , which should be used to passed auth provider id to URL + * defined by {@link baseAuthUrl}. + */ + public $providerIdGetParamName = 'provider'; + /** + * @var callable PHP callback, which should be triggered in case of successful authentication. + */ + public $successCallback; + /** + * @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 $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; + } + + /** + * 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(); + } + + /** + * Runs the action. + */ + public function run() + { + if (!empty($_GET[$this->providerIdGetParamName])) { + $providerId = $_GET[$this->providerIdGetParamName]; + /** @var \yii\authclient\provider\Collection $providerCollection */ + $providerCollection = Yii::$app->getComponent($this->providerCollection); + if (!$providerCollection->hasProvider($providerId)) { + throw new NotFoundHttpException("Unknown auth provider '{$providerId}'"); + } + $provider = $providerCollection->getProvider($providerId); + return $this->authenticate($provider); + } else { + throw new NotFoundHttpException(); + } + } + + /** + * @param mixed $provider + * @throws \yii\base\NotSupportedException + */ + protected function authenticate($provider) + { + if ($provider instanceof OpenId) { + return $this->authenticateOpenId($provider); + } elseif ($provider instanceof OAuth2) { + return $this->authenticateOAuth2($provider); + } elseif ($provider instanceof OAuth1) { + return $this->authenticateOAuth1($provider); + } else { + throw new NotSupportedException('Provider "' . get_class($provider) . '" is not supported.'); + } + } + + /** + * @param mixed $provider + * @return \yii\web\Response + */ + protected function authenticateSuccess($provider) + { + call_user_func($this->successCallback, $provider); + return $this->redirectSuccess(); + } + + /** + * Redirect to the given URL or simply close the popup window. + * @param mixed $url URL to redirect, could be a string or array config to generate a valid URL. + * @param boolean $enforceRedirect indicates if redirect should be performed even in case of popup window. + * @return \yii\web\Response response instance. + */ + public function redirect($url, $enforceRedirect = true) + { + $viewData = [ + 'url' => $url, + 'enforceRedirect' => $enforceRedirect, + ]; + $viewFile = __DIR__ . DIRECTORY_SEPARATOR . 'provider' . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'redirect.php'; + + $response = Yii::$app->getResponse(); + $response->content = Yii::$app->getView()->renderFile($viewFile, $viewData); + return $response; + } + + /** + * Redirect to the URL. If URL is null, {@link successUrl} will be used. + * @param string $url URL to redirect. + * @return \yii\web\Response response instance. + */ + public function redirectSuccess($url = null) + { + if ($url === null) { + $url = $this->getSuccessUrl(); + } + return $this->redirect($url); + } + + /** + * Redirect to the {@link cancelUrl} or simply close the popup window. + * @param string $url URL to redirect. + * @return \yii\web\Response response instance. + */ + public function redirectCancel($url = null) + { + if ($url === null) { + $url = $this->getCancelUrl(); + } + return $this->redirect($url, false); + } + + /** + * @param OpenId $provider provider instance. + * @return \yii\web\Response action response. + * @throws Exception on failure + * @throws \yii\web\HttpException + */ + protected function authenticateOpenId($provider) + { + if (!empty($_REQUEST['openid_mode'])) { + switch ($_REQUEST['openid_mode']) { + case 'id_res': + if ($provider->validate()) { + $attributes = array( + 'id' => $provider->identity + ); + $rawAttributes = $provider->getAttributes(); + foreach ($provider->getRequiredAttributes() as $openIdAttributeName) { + if (isset($rawAttributes[$openIdAttributeName])) { + $attributes[$openIdAttributeName] = $rawAttributes[$openIdAttributeName]; + } else { + throw new Exception('Unable to complete the authentication because the required data was not received.'); + } + } + $provider->setAttributes($attributes); + $provider->isAuthenticated = true; + return $this->authenticateSuccess($provider); + } else { + throw new Exception('Unable to complete the authentication because the required data was not received.'); + } + break; + case 'cancel': + $this->redirectCancel(); + break; + default: + throw new HttpException(400); + break; + } + } else { + $provider->identity = $provider->authUrl; // Setting identifier + $provider->required = []; // Try to get info from openid provider + foreach ($provider->getRequiredAttributes() as $openIdAttributeName) { + $this->required[] = $openIdAttributeName; + } + $request = Yii::$app->getRequest(); + $provider->realm = $request->getHostInfo(); + $provider->returnUrl = $provider->realm . $request->getUrl(); // getting return URL + + $url = $provider->authUrl(); + return Yii::$app->getResponse()->redirect($url); + } + return $this->redirectCancel(); + } + + /** + * @param OAuth1 $provider + * @return \yii\web\Response + */ + protected function authenticateOAuth1($provider) + { + // user denied error + if (isset($_GET['denied'])) { + return $this->redirectCancel(); + } + + if (isset($_REQUEST['oauth_token'])) { + $oauthToken = $_REQUEST['oauth_token']; + } + + if (!isset($oauthToken)) { + // Get request token. + $requestToken = $provider->fetchRequestToken(); + // Get authorization URL. + $url = $provider->buildAuthUrl($requestToken); + // Redirect to authorization URL. + return Yii::$app->getResponse()->redirect($url); + } else { + // Upgrade to access token. + $accessToken = $provider->fetchAccessToken(); + $provider->isAuthenticated = true; + return $this->authenticateSuccess($provider); + } + } + + /** + * @param OAuth2 $provider + * @return \yii\web\Response + * @throws \yii\base\Exception + */ + protected function authenticateOAuth2($provider) + { + if (isset($_GET['error'])) { + if ($_GET['error'] == 'access_denied') { + // user denied error + return $this->redirectCancel(); + } else { + // request error + if (isset($_GET['error_description'])) { + $errorMessage = $_GET['error_description']; + } elseif (isset($_GET['error_message'])) { + $errorMessage = $_GET['error_message']; + } else { + $errorMessage = http_build_query($_GET); + } + throw new Exception('Auth error: ' . $errorMessage); + } + } + + // Get the access_token and save them to the session. + if (isset($_GET['code'])) { + $code = $_GET['code']; + $token = $provider->fetchAccessToken($code); + if (!empty($token)) { + $provider->isAuthenticated = true; + return $this->authenticateSuccess($provider); + } else { + return $this->redirectCancel(); + } + } else { + $url = $provider->buildAuthUrl(); + return Yii::$app->getResponse()->redirect($url); + } + } +} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/AuthActionTest.php b/tests/unit/extensions/authclient/AuthActionTest.php new file mode 100644 index 0000000..955f643 --- /dev/null +++ b/tests/unit/extensions/authclient/AuthActionTest.php @@ -0,0 +1,68 @@ + [ + 'user' => [ + 'identityClass' => '\yii\web\IdentityInterface' + ], + 'request' => [ + 'hostInfo' => 'http://testdomain.com', + 'scriptUrl' => '/index.php', + ], + ] + ]; + $this->mockApplication($config, '\yii\web\Application'); + } + + public function testSetGet() + { + $action = new AuthAction(null, null); + + $successUrl = 'http://test.success.url'; + $action->setSuccessUrl($successUrl); + $this->assertEquals($successUrl, $action->getSuccessUrl(), 'Unable to setup success URL!'); + + $cancelUrl = 'http://test.cancel.url'; + $action->setCancelUrl($cancelUrl); + $this->assertEquals($cancelUrl, $action->getCancelUrl(), 'Unable to setup cancel URL!'); + } + + /** + * @depends testSetGet + */ + public function testGetDefaultSuccessUrl() + { + $action = new AuthAction(null, null); + + $this->assertNotEmpty($action->getSuccessUrl(), 'Unable to get default success URL!'); + } + + /** + * @depends testSetGet + */ + public function testGetDefaultCancelUrl() + { + $action = new AuthAction(null, null); + + $this->assertNotEmpty($action->getSuccessUrl(), 'Unable to get default cancel URL!'); + } + + public function testRedirect() + { + $action = new AuthAction(null, null); + + $url = 'http://test.url'; + $response = $action->redirect($url, true); + + $this->assertContains($url, $response->content); + } +} \ No newline at end of file From 776a92502fdedb2dfce28f7ca9a4e5b311653d7f Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Mon, 16 Dec 2013 13:26:25 +0200 Subject: [PATCH 09/37] "yii\authclient" structure refactored. --- extensions/yii/authclient/AuthAction.php | 11 +- extensions/yii/authclient/ClientInterface.php | 52 +++++ extensions/yii/authclient/ClientTrait.php | 145 ++++++++++++++ extensions/yii/authclient/Collection.php | 107 ++++++++++ extensions/yii/authclient/OpenId.php | 11 +- extensions/yii/authclient/provider/Collection.php | 105 ---------- extensions/yii/authclient/provider/OAuth1.php | 51 ----- extensions/yii/authclient/provider/OAuth2.php | 58 ------ extensions/yii/authclient/provider/OpenId.php | 74 ------- .../yii/authclient/provider/ProviderInterface.php | 73 ------- .../yii/authclient/provider/ProviderTrait.php | 221 --------------------- .../yii/authclient/provider/views/redirect.php | 38 ---- extensions/yii/authclient/views/redirect.php | 38 ++++ .../unit/extensions/authclient/ClientTraitTest.php | 58 ++++++ .../unit/extensions/authclient/CollectionTest.php | 90 +++++++++ .../authclient/provider/CollectionTest.php | 90 --------- .../authclient/provider/ProviderTraitTest.php | 90 --------- 17 files changed, 499 insertions(+), 813 deletions(-) create mode 100644 extensions/yii/authclient/ClientInterface.php create mode 100644 extensions/yii/authclient/ClientTrait.php create mode 100644 extensions/yii/authclient/Collection.php delete mode 100644 extensions/yii/authclient/provider/Collection.php delete mode 100644 extensions/yii/authclient/provider/OAuth1.php delete mode 100644 extensions/yii/authclient/provider/OAuth2.php delete mode 100644 extensions/yii/authclient/provider/OpenId.php delete mode 100644 extensions/yii/authclient/provider/ProviderInterface.php delete mode 100644 extensions/yii/authclient/provider/ProviderTrait.php delete mode 100644 extensions/yii/authclient/provider/views/redirect.php create mode 100644 extensions/yii/authclient/views/redirect.php create mode 100644 tests/unit/extensions/authclient/ClientTraitTest.php create mode 100644 tests/unit/extensions/authclient/CollectionTest.php delete mode 100644 tests/unit/extensions/authclient/provider/CollectionTest.php delete mode 100644 tests/unit/extensions/authclient/provider/ProviderTraitTest.php diff --git a/extensions/yii/authclient/AuthAction.php b/extensions/yii/authclient/AuthAction.php index f3dc453..6109bd9 100644 --- a/extensions/yii/authclient/AuthAction.php +++ b/extensions/yii/authclient/AuthAction.php @@ -159,7 +159,7 @@ class AuthAction extends Action 'url' => $url, 'enforceRedirect' => $enforceRedirect, ]; - $viewFile = __DIR__ . DIRECTORY_SEPARATOR . 'provider' . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'redirect.php'; + $viewFile = __DIR__ . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'redirect.php'; $response = Yii::$app->getResponse(); $response->content = Yii::$app->getView()->renderFile($viewFile, $viewData); @@ -208,7 +208,7 @@ class AuthAction extends Action 'id' => $provider->identity ); $rawAttributes = $provider->getAttributes(); - foreach ($provider->getRequiredAttributes() as $openIdAttributeName) { + foreach ($provider->requiredAttributes as $openIdAttributeName) { if (isset($rawAttributes[$openIdAttributeName])) { $attributes[$openIdAttributeName] = $rawAttributes[$openIdAttributeName]; } else { @@ -216,7 +216,6 @@ class AuthAction extends Action } } $provider->setAttributes($attributes); - $provider->isAuthenticated = true; return $this->authenticateSuccess($provider); } else { throw new Exception('Unable to complete the authentication because the required data was not received.'); @@ -231,10 +230,6 @@ class AuthAction extends Action } } else { $provider->identity = $provider->authUrl; // Setting identifier - $provider->required = []; // Try to get info from openid provider - foreach ($provider->getRequiredAttributes() as $openIdAttributeName) { - $this->required[] = $openIdAttributeName; - } $request = Yii::$app->getRequest(); $provider->realm = $request->getHostInfo(); $provider->returnUrl = $provider->realm . $request->getUrl(); // getting return URL @@ -270,7 +265,6 @@ class AuthAction extends Action } else { // Upgrade to access token. $accessToken = $provider->fetchAccessToken(); - $provider->isAuthenticated = true; return $this->authenticateSuccess($provider); } } @@ -304,7 +298,6 @@ class AuthAction extends Action $code = $_GET['code']; $token = $provider->fetchAccessToken($code); if (!empty($token)) { - $provider->isAuthenticated = true; return $this->authenticateSuccess($provider); } else { return $this->redirectCancel(); diff --git a/extensions/yii/authclient/ClientInterface.php b/extensions/yii/authclient/ClientInterface.php new file mode 100644 index 0000000..2b08e1e --- /dev/null +++ b/extensions/yii/authclient/ClientInterface.php @@ -0,0 +1,52 @@ + + * @since 2.0 + */ +interface ClientInterface +{ + /** + * @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); + + /** + * @return array list of user attributes + */ + public function getUserAttributes(); +} \ No newline at end of file diff --git a/extensions/yii/authclient/ClientTrait.php b/extensions/yii/authclient/ClientTrait.php new file mode 100644 index 0000000..cdd3150 --- /dev/null +++ b/extensions/yii/authclient/ClientTrait.php @@ -0,0 +1,145 @@ + + * @since 2.0 + */ +trait ClientTrait +{ + /** + * @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 array authenticated user attributes. + */ + private $_userAttributes; + + /** + * @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; + } + + /** + * @return array list of user attributes + */ + public function getUserAttributes() + { + if ($this->_userAttributes === null) { + $this->_userAttributes = $this->initUserAttributes(); + } + return $this->_userAttributes; + } + + /** + * @param array $userAttributes list of user attributes + */ + public function setUserAttributes(array $userAttributes) + { + $this->_userAttributes = $userAttributes; + } + + /** + * 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)); + } + + /** + * Initializes authenticated user attributes. + * @return array auth user attributes. + */ + protected function initUserAttributes() + { + throw new NotSupportedException('Method "' . get_class($this) . '::' . __FUNCTION__ . '" not implemented.'); + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/Collection.php b/extensions/yii/authclient/Collection.php new file mode 100644 index 0000000..d94e619 --- /dev/null +++ b/extensions/yii/authclient/Collection.php @@ -0,0 +1,107 @@ + [ + * 'auth' => [ + * 'class' => 'yii\authclient\Collection', + * 'clients' => [ + * 'google' => [ + * 'class' => 'yii\authclient\clients\GoogleOpenId' + * ], + * 'facebook' => [ + * 'class' => 'yii\authclient\clients\Facebook', + * 'clientId' => 'facebook_client_id', + * 'clientSecret' => 'facebook_client_secret', + * ], + * ], + * ] + * ... + * ] + * ~~~ + * + * @property array $clients list of Auth clients with their configuration in format: 'clientId' => [...] + * + * @author Paul Klimov + * @since 2.0 + */ +class Collection extends Component +{ + /** + * @var array list of Auth clients with their configuration in format: 'clientId' => [...] + */ + private $_clients = []; + + /** + * @param array $clients list of auth clients + */ + public function setClients(array $clients) + { + $this->_clients = $clients; + } + + /** + * @return ClientInterface[] list of auth clients. + */ + public function getClients() + { + $clients = []; + foreach ($this->_clients as $id => $client) { + $clients[$id] = $this->getClient($id); + } + return $clients; + } + + /** + * @param string $id service id. + * @return ClientInterface auth client instance. + * @throws InvalidParamException on non existing client request. + */ + public function getClient($id) + { + if (!array_key_exists($id, $this->_clients)) { + throw new InvalidParamException("Unknown auth client '{$id}'."); + } + if (!is_object($this->_clients[$id])) { + $this->_clients[$id] = $this->createClient($id, $this->_clients[$id]); + } + return $this->_clients[$id]; + } + + /** + * Checks if client exists in the hub. + * @param string $id client id. + * @return boolean whether client exist. + */ + public function hasClient($id) + { + return array_key_exists($id, $this->_clients); + } + + /** + * Creates auth client instance from its array configuration. + * @param string $id auth client id. + * @param array $config auth client instance configuration. + * @return ClientInterface auth client instance. + */ + protected function createClient($id, $config) + { + $config['id'] = $id; + return Yii::createObject($config); + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/OpenId.php b/extensions/yii/authclient/OpenId.php index f421379..fa87204 100644 --- a/extensions/yii/authclient/OpenId.php +++ b/extensions/yii/authclient/OpenId.php @@ -27,7 +27,10 @@ use yii\base\NotSupportedException; */ class OpenId extends Component { - public $required = []; + /** + * @var array list of attributes, which should be requested from server. + */ + public $requiredAttributes = []; public $optional = []; public $verify_peer; public $capath; @@ -501,9 +504,9 @@ class OpenId extends Component # 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) { + if ($this->requiredAttributes) { $params['openid.sreg.required'] = []; - foreach ($this->required as $required) { + foreach ($this->requiredAttributes as $required) { if (!isset(self::$axToSregMap[$required])) { continue; } @@ -528,7 +531,7 @@ class OpenId extends Component protected function axParams() { $params = []; - if ($this->required || $this->optional) { + if ($this->requiredAttributes || $this->optional) { $params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0'; $params['openid.ax.mode'] = 'fetch_request'; $this->aliases = []; diff --git a/extensions/yii/authclient/provider/Collection.php b/extensions/yii/authclient/provider/Collection.php deleted file mode 100644 index 309eae5..0000000 --- a/extensions/yii/authclient/provider/Collection.php +++ /dev/null @@ -1,105 +0,0 @@ - [ - * 'auth' => [ - * 'class' => 'yii\authclient\provider\Collection', - * 'providers' => [ - * 'google' => [ - * 'class' => 'yii\authclient\provider\GoogleOpenId' - * ], - * 'facebook' => [ - * 'class' => 'yii\authclient\provider\Facebook', - * 'clientId' => 'facebook_client_id', - * 'clientSecret' => 'facebook_client_secret', - * ], - * ], - * ] - * ... - * ] - * ~~~ - * - * @author Paul Klimov - * @since 2.0 - */ -class Collection extends Component -{ - /** - * @var array list of Auth providers with their configuration in format: 'providerId' => [...] - */ - private $_providers = []; - - /** - * @param array $providers list of auth providers - */ - public function setProviders(array $providers) - { - $this->_providers = $providers; - } - - /** - * @return ProviderInterface[] list of auth providers. - */ - public function getProviders() - { - $providers = []; - foreach ($this->_providers as $id => $provider) { - $providers[$id] = $this->getProvider($id); - } - return $providers; - } - - /** - * @param string $id service id. - * @return ProviderInterface auth service instance. - * @throws InvalidParamException on non existing provider request. - */ - public function getProvider($id) - { - if (!array_key_exists($id, $this->_providers)) { - throw new InvalidParamException("Unknown auth provider '{$id}'."); - } - if (!is_object($this->_providers[$id])) { - $this->_providers[$id] = $this->createProvider($id, $this->_providers[$id]); - } - return $this->_providers[$id]; - } - - /** - * Checks if provider exists in the hub. - * @param string $id provider id. - * @return boolean whether provider exist. - */ - public function hasProvider($id) - { - return array_key_exists($id, $this->_providers); - } - - /** - * Creates auth provider instance from its array configuration. - * @param string $id auth provider id. - * @param array $config auth provider instance configuration. - * @return ProviderInterface auth provider instance. - */ - protected function createProvider($id, $config) - { - $config['id'] = $id; - return Yii::createObject($config); - } -} \ No newline at end of file diff --git a/extensions/yii/authclient/provider/OAuth1.php b/extensions/yii/authclient/provider/OAuth1.php deleted file mode 100644 index 4220c39..0000000 --- a/extensions/yii/authclient/provider/OAuth1.php +++ /dev/null @@ -1,51 +0,0 @@ - - * @since 2.0 - */ -class OAuth1 extends \yii\authclient\OAuth1 implements ProviderInterface -{ - use ProviderTrait; - - /** - * @inheritdoc - */ - public function authenticate() - { - // user denied error - if (isset($_GET['denied'])) { - return $this->redirectCancel(); - } - - if (isset($_REQUEST['oauth_token'])) { - $oauthToken = $_REQUEST['oauth_token']; - } - - if (!isset($oauthToken)) { - // Get request token. - $requestToken = $this->fetchRequestToken(); - // Get authorization URL. - $url = $this->buildAuthUrl($requestToken); - // Redirect to authorization URL. - return Yii::$app->getResponse()->redirect($url); - } else { - // Upgrade to access token. - $accessToken = $this->fetchAccessToken(); - $this->isAuthenticated = true; - } - - return $this->isAuthenticated; - } -} \ No newline at end of file diff --git a/extensions/yii/authclient/provider/OAuth2.php b/extensions/yii/authclient/provider/OAuth2.php deleted file mode 100644 index e55328e..0000000 --- a/extensions/yii/authclient/provider/OAuth2.php +++ /dev/null @@ -1,58 +0,0 @@ - - * @since 2.0 - */ -class OAuth2 extends \yii\authclient\OAuth2 implements ProviderInterface -{ - use ProviderTrait; - - /** - * @inheritdoc - */ - public function authenticate() - { - if (isset($_GET['error'])) { - if ($_GET['error'] == 'access_denied') { - // user denied error - return $this->redirectCancel(); - } else { - // request error - if (isset($_GET['error_description'])) { - $errorMessage = $_GET['error_description']; - } elseif (isset($_GET['error_message'])) { - $errorMessage = $_GET['error_message']; - } else { - $errorMessage = http_build_query($_GET); - } - throw new Exception('Auth error: ' . $errorMessage); - } - } - - // Get the access_token and save them to the session. - if (isset($_GET['code'])) { - $code = $_GET['code']; - $token = $this->fetchAccessToken($code); - if (!empty($token)) { - $this->isAuthenticated = true; - } - } else { - $url = $this->buildAuthUrl(); - return Yii::$app->getResponse()->redirect($url); - } - - return $this->isAuthenticated; - } -} \ No newline at end of file diff --git a/extensions/yii/authclient/provider/OpenId.php b/extensions/yii/authclient/provider/OpenId.php deleted file mode 100644 index ff1985c..0000000 --- a/extensions/yii/authclient/provider/OpenId.php +++ /dev/null @@ -1,74 +0,0 @@ - - * @since 2.0 - */ -class OpenId extends \yii\authclient\OpenId implements ProviderInterface -{ - use ProviderTrait; - - /** - * @inheritdoc - */ - public function authenticate() - { - if (!empty($_REQUEST['openid_mode'])) { - switch ($_REQUEST['openid_mode']) { - case 'id_res': - if ($this->validate()) { - $attributes = array( - 'id' => $this->identity - ); - $rawAttributes = $this->getAttributes(); - foreach ($this->getRequiredAttributes() as $openIdAttributeName) { - if (isset($rawAttributes[$openIdAttributeName])) { - $attributes[$openIdAttributeName] = $rawAttributes[$openIdAttributeName]; - } else { - throw new Exception('Unable to complete the authentication because the required data was not received.'); - } - } - $this->setAttributes($attributes); - $this->isAuthenticated = true; - return true; - } else { - throw new Exception('Unable to complete the authentication because the required data was not received.'); - } - break; - case 'cancel': - $this->redirectCancel(); - break; - default: - throw new HttpException(400); - break; - } - } else { - $this->identity = $this->authUrl; // Setting identifier - $this->required = []; // Try to get info from openid provider - foreach ($this->getRequiredAttributes() as $openIdAttributeName) { - $this->required[] = $openIdAttributeName; - } - $request = Yii::$app->getRequest(); - $this->realm = $request->getHostInfo(); - $this->returnUrl = $this->realm . $request->getUrl(); // getting return URL - - $url = $this->authUrl(); - return Yii::$app->getResponse()->redirect($url); - } - - return false; - } -} \ No newline at end of file diff --git a/extensions/yii/authclient/provider/ProviderInterface.php b/extensions/yii/authclient/provider/ProviderInterface.php deleted file mode 100644 index b3f2b27..0000000 --- a/extensions/yii/authclient/provider/ProviderInterface.php +++ /dev/null @@ -1,73 +0,0 @@ - - * @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 \yii\web\Response|boolean response instance or 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 deleted file mode 100644 index 129fa0d..0000000 --- a/extensions/yii/authclient/provider/ProviderTrait.php +++ /dev/null @@ -1,221 +0,0 @@ - - * @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(); - } - - /** - * Redirect to the given URL or simply close the popup window. - * @param mixed $url URL to redirect, could be a string or array config to generate a valid URL. - * @param boolean $enforceRedirect indicates if redirect should be performed even in case of popup window. - * @return \yii\web\Response response instance. - */ - public function redirect($url, $enforceRedirect = true) - { - $viewData = [ - 'url' => $url, - 'enforceRedirect' => $enforceRedirect, - ]; - $viewFile = __DIR__ . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'redirect.php'; - - $response = Yii::$app->getResponse(); - $response->content = Yii::$app->getView()->renderFile($viewFile, $viewData); - return $response; - } - - /** - * Redirect to the URL. If URL is null, {@link successUrl} will be used. - * @param string $url URL to redirect. - * @return \yii\web\Response response instance. - */ - public function redirectSuccess($url = null) - { - if ($url === null) { - $url = $this->getSuccessUrl(); - } - return $this->redirect($url); - } - - /** - * Redirect to the {@link cancelUrl} or simply close the popup window. - * @param string $url URL to redirect. - * @return \yii\web\Response response instance. - */ - public function redirectCancel($url = null) - { - if ($url === null) { - $url = $this->getCancelUrl(); - } - return $this->redirect($url, false); - } -} \ No newline at end of file diff --git a/extensions/yii/authclient/provider/views/redirect.php b/extensions/yii/authclient/provider/views/redirect.php deleted file mode 100644 index e85aa46..0000000 --- a/extensions/yii/authclient/provider/views/redirect.php +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/extensions/yii/authclient/views/redirect.php b/extensions/yii/authclient/views/redirect.php new file mode 100644 index 0000000..e85aa46 --- /dev/null +++ b/extensions/yii/authclient/views/redirect.php @@ -0,0 +1,38 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/unit/extensions/authclient/ClientTraitTest.php b/tests/unit/extensions/authclient/ClientTraitTest.php new file mode 100644 index 0000000..404533f --- /dev/null +++ b/tests/unit/extensions/authclient/ClientTraitTest.php @@ -0,0 +1,58 @@ + [ + 'user' => [ + 'identityClass' => '\yii\web\IdentityInterface' + ], + 'request' => [ + 'hostInfo' => 'http://testdomain.com', + 'scriptUrl' => '/index.php', + ], + ] + ]; + $this->mockApplication($config, '\yii\web\Application'); + } + + public function testSetGet() + { + $provider = new Client(); + + $id = 'test_id'; + $provider->setId($id); + $this->assertEquals($id, $provider->getId(), 'Unable to setup id!'); + + $name = 'test_name'; + $provider->setName($name); + $this->assertEquals($name, $provider->getName(), 'Unable to setup name!'); + + $title = 'test_title'; + $provider->setTitle($title); + $this->assertEquals($title, $provider->getTitle(), 'Unable to setup title!'); + } + + public function testGetDescriptiveData() + { + $provider = new Client(); + + $this->assertNotEmpty($provider->getName(), 'Unable to get name!'); + $this->assertNotEmpty($provider->getTitle(), 'Unable to get title!'); + } +} + +class Client extends Object implements ClientInterface +{ + use ClientTrait; + + public function authenticate() {} +} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/CollectionTest.php b/tests/unit/extensions/authclient/CollectionTest.php new file mode 100644 index 0000000..14f65a0 --- /dev/null +++ b/tests/unit/extensions/authclient/CollectionTest.php @@ -0,0 +1,90 @@ + new TestClient(), + 'testClient2' => new TestClient(), + ]; + $collection->setClients($clients); + $this->assertEquals($clients, $collection->getClients(), 'Unable to setup clients!'); + } + + /** + * @depends testSetGet + */ + public function testGetProviderById() + { + $collection = new Collection(); + + $clientId = 'testClientId'; + $client = new TestClient(); + $clients = [ + $clientId => $client + ]; + $collection->setClients($clients); + + $this->assertEquals($client, $collection->getClient($clientId), 'Unable to get client by id!'); + } + + /** + * @depends testGetProviderById + */ + public function testCreateProvider() + { + $collection = new Collection(); + + $clientId = 'testClientId'; + $clientClassName = TestClient::className(); + $clients = [ + $clientId => [ + 'class' => $clientClassName + ] + ]; + $collection->setClients($clients); + + $provider = $collection->getClient($clientId); + $this->assertTrue(is_object($provider), 'Unable to create client by config!'); + $this->assertTrue(is_a($provider, $clientClassName), 'Client has wrong class name!'); + } + + /** + * @depends testSetGet + */ + public function testHasProvider() + { + $collection = new Collection(); + + $clientName = 'testClientName'; + $clients = [ + $clientName => [ + 'class' => 'TestClient1' + ], + ]; + $collection->setClients($clients); + + $this->assertTrue($collection->hasClient($clientName), 'Existing client check fails!'); + $this->assertFalse($collection->hasClient('unExistingClientName'), 'Not existing client check fails!'); + } +} + +class TestClient extends Object implements ClientInterface +{ + use ClientTrait; + + public function authenticate() {} +} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/provider/CollectionTest.php b/tests/unit/extensions/authclient/provider/CollectionTest.php deleted file mode 100644 index 9c65e59..0000000 --- a/tests/unit/extensions/authclient/provider/CollectionTest.php +++ /dev/null @@ -1,90 +0,0 @@ - new TestProvider(), - 'testProvider2' => new TestProvider(), - ]; - $collection->setProviders($providers); - $this->assertEquals($providers, $collection->getProviders(), 'Unable to setup providers!'); - } - - /** - * @depends testSetGet - */ - public function testGetProviderById() - { - $collection = new Collection(); - - $providerId = 'testProviderId'; - $provider = new TestProvider(); - $providers = [ - $providerId => $provider - ]; - $collection->setProviders($providers); - - $this->assertEquals($provider, $collection->getProvider($providerId), 'Unable to get provider by id!'); - } - - /** - * @depends testGetProviderById - */ - public function testCreateProvider() - { - $collection = new Collection(); - - $providerId = 'testProviderId'; - $providerClassName = TestProvider::className(); - $providers = [ - $providerId => [ - 'class' => $providerClassName - ] - ]; - $collection->setProviders($providers); - - $provider = $collection->getProvider($providerId); - $this->assertTrue(is_object($provider), 'Unable to create provider by config!'); - $this->assertTrue(is_a($provider, $providerClassName), 'Provider has wrong class name!'); - } - - /** - * @depends testSetGet - */ - public function testHasProvider() - { - $collection = new Collection(); - - $providerName = 'testProviderName'; - $providers = [ - $providerName => [ - 'class' => 'TestProvider1' - ], - ]; - $collection->setProviders($providers); - - $this->assertTrue($collection->hasProvider($providerName), 'Existing provider check fails!'); - $this->assertFalse($collection->hasProvider('unExistingProviderName'), 'Not existing provider check fails!'); - } -} - -class TestProvider extends Object implements ProviderInterface -{ - use ProviderTrait; - - public function authenticate() {} -} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/provider/ProviderTraitTest.php b/tests/unit/extensions/authclient/provider/ProviderTraitTest.php deleted file mode 100644 index 8289eb3..0000000 --- a/tests/unit/extensions/authclient/provider/ProviderTraitTest.php +++ /dev/null @@ -1,90 +0,0 @@ - [ - 'user' => [ - 'identityClass' => '\yii\web\IdentityInterface' - ], - 'request' => [ - 'hostInfo' => 'http://testdomain.com', - 'scriptUrl' => '/index.php', - ], - ] - ]; - $this->mockApplication($config, '\yii\web\Application'); - } - - public function testSetGet() - { - $provider = new Provider(); - - $id = 'test_service_id'; - $provider->setId($id); - $this->assertEquals($id, $provider->getId(), 'Unable to setup id!'); - - $successUrl = 'http://test.success.url'; - $provider->setSuccessUrl($successUrl); - $this->assertEquals($successUrl, $provider->getSuccessUrl(), 'Unable to setup success URL!'); - - $cancelUrl = 'http://test.cancel.url'; - $provider->setCancelUrl($cancelUrl); - $this->assertEquals($cancelUrl, $provider->getCancelUrl(), 'Unable to setup cancel URL!'); - } - - public function testGetDescriptiveData() - { - $provider = new Provider(); - - $this->assertNotEmpty($provider->getName(), 'Unable to get name!'); - $this->assertNotEmpty($provider->getTitle(), 'Unable to get title!'); - } - - /** - * @depends testSetGet - */ - public function testGetDefaultSuccessUrl() - { - $provider = new Provider(); - - $this->assertNotEmpty($provider->getSuccessUrl(), 'Unable to get default success URL!'); - } - - /** - * @depends testSetGet - */ - public function testGetDefaultCancelUrl() - { - $provider = new Provider(); - - $this->assertNotEmpty($provider->getSuccessUrl(), 'Unable to get default cancel URL!'); - } - - public function testRedirect() - { - $provider = new Provider(); - - $url = 'http://test.url'; - $response = $provider->redirect($url, true); - - $this->assertContains($url, $response->content); - } -} - -class Provider extends Object implements ProviderInterface -{ - use ProviderTrait; - - public function authenticate() {} -} \ No newline at end of file From c8a0591f488b40c0d5287c4a89f196530523dab8 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Mon, 16 Dec 2013 13:55:24 +0200 Subject: [PATCH 10/37] "yii\authclient\AuthAction" fixed. "yii\authclient\ClientInterface" applied. --- extensions/yii/authclient/AuthAction.php | 50 +++++++++++++++----------------- extensions/yii/authclient/BaseOAuth.php | 4 ++- extensions/yii/authclient/OpenId.php | 4 ++- 3 files changed, 30 insertions(+), 28 deletions(-) diff --git a/extensions/yii/authclient/AuthAction.php b/extensions/yii/authclient/AuthAction.php index 6109bd9..9ccd848 100644 --- a/extensions/yii/authclient/AuthAction.php +++ b/extensions/yii/authclient/AuthAction.php @@ -23,15 +23,13 @@ use Yii; class AuthAction extends Action { /** - * @var string name of the auth provider collection application component. - * This component will be used to fetch {@link services} value if it is not set. + * @var string name of the auth client collection application component. */ - public $providerCollection; + public $clientCollection; /** - * @var string name of the GET param , which should be used to passed auth provider id to URL - * defined by {@link baseAuthUrl}. + * @var string name of the GET param, which is used to passed auth client id to this action. */ - public $providerIdGetParamName = 'provider'; + public $clientIdGetParamName = 'client_id'; /** * @var callable PHP callback, which should be triggered in case of successful authentication. */ @@ -106,15 +104,15 @@ class AuthAction extends Action */ public function run() { - if (!empty($_GET[$this->providerIdGetParamName])) { - $providerId = $_GET[$this->providerIdGetParamName]; - /** @var \yii\authclient\provider\Collection $providerCollection */ - $providerCollection = Yii::$app->getComponent($this->providerCollection); - if (!$providerCollection->hasProvider($providerId)) { - throw new NotFoundHttpException("Unknown auth provider '{$providerId}'"); + if (!empty($_GET[$this->clientIdGetParamName])) { + $clientId = $_GET[$this->clientIdGetParamName]; + /** @var \yii\authclient\Collection $collection */ + $collection = Yii::$app->getComponent($this->clientCollection); + if (!$collection->hasClient($clientId)) { + throw new NotFoundHttpException("Unknown auth client '{$clientId}'"); } - $provider = $providerCollection->getProvider($providerId); - return $this->authenticate($provider); + $client = $collection->getClient($clientId); + return $this->auth($client); } else { throw new NotFoundHttpException(); } @@ -124,14 +122,14 @@ class AuthAction extends Action * @param mixed $provider * @throws \yii\base\NotSupportedException */ - protected function authenticate($provider) + protected function auth($provider) { if ($provider instanceof OpenId) { - return $this->authenticateOpenId($provider); + return $this->authOpenId($provider); } elseif ($provider instanceof OAuth2) { - return $this->authenticateOAuth2($provider); + return $this->authOAuth2($provider); } elseif ($provider instanceof OAuth1) { - return $this->authenticateOAuth1($provider); + return $this->authOAuth1($provider); } else { throw new NotSupportedException('Provider "' . get_class($provider) . '" is not supported.'); } @@ -141,7 +139,7 @@ class AuthAction extends Action * @param mixed $provider * @return \yii\web\Response */ - protected function authenticateSuccess($provider) + protected function authSuccess($provider) { call_user_func($this->successCallback, $provider); return $this->redirectSuccess(); @@ -198,7 +196,7 @@ class AuthAction extends Action * @throws Exception on failure * @throws \yii\web\HttpException */ - protected function authenticateOpenId($provider) + protected function authOpenId($provider) { if (!empty($_REQUEST['openid_mode'])) { switch ($_REQUEST['openid_mode']) { @@ -215,8 +213,8 @@ class AuthAction extends Action throw new Exception('Unable to complete the authentication because the required data was not received.'); } } - $provider->setAttributes($attributes); - return $this->authenticateSuccess($provider); + $provider->setUserAttributes($attributes); + return $this->authSuccess($provider); } else { throw new Exception('Unable to complete the authentication because the required data was not received.'); } @@ -244,7 +242,7 @@ class AuthAction extends Action * @param OAuth1 $provider * @return \yii\web\Response */ - protected function authenticateOAuth1($provider) + protected function authOAuth1($provider) { // user denied error if (isset($_GET['denied'])) { @@ -265,7 +263,7 @@ class AuthAction extends Action } else { // Upgrade to access token. $accessToken = $provider->fetchAccessToken(); - return $this->authenticateSuccess($provider); + return $this->authSuccess($provider); } } @@ -274,7 +272,7 @@ class AuthAction extends Action * @return \yii\web\Response * @throws \yii\base\Exception */ - protected function authenticateOAuth2($provider) + protected function authOAuth2($provider) { if (isset($_GET['error'])) { if ($_GET['error'] == 'access_denied') { @@ -298,7 +296,7 @@ class AuthAction extends Action $code = $_GET['code']; $token = $provider->fetchAccessToken($code); if (!empty($token)) { - return $this->authenticateSuccess($provider); + return $this->authSuccess($provider); } else { return $this->redirectCancel(); } diff --git a/extensions/yii/authclient/BaseOAuth.php b/extensions/yii/authclient/BaseOAuth.php index 1f02239..4c0a025 100644 --- a/extensions/yii/authclient/BaseOAuth.php +++ b/extensions/yii/authclient/BaseOAuth.php @@ -21,8 +21,10 @@ use yii\helpers\Json; * @author Paul Klimov * @since 2.0 */ -abstract class BaseOAuth extends Component +abstract class BaseOAuth extends Component implements ClientInterface { + use ClientTrait; + 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 diff --git a/extensions/yii/authclient/OpenId.php b/extensions/yii/authclient/OpenId.php index fa87204..b614578 100644 --- a/extensions/yii/authclient/OpenId.php +++ b/extensions/yii/authclient/OpenId.php @@ -25,8 +25,10 @@ use yii\base\NotSupportedException; * @author Paul Klimov * @since 2.0 */ -class OpenId extends Component +class OpenId extends Component implements ClientInterface { + use ClientTrait; + /** * @var array list of attributes, which should be requested from server. */ From fc0d3bfeb601306914625ac15a07ad3e6be21738 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Mon, 16 Dec 2013 17:24:29 +0200 Subject: [PATCH 11/37] "yii\authclient\clients\GoogleOpenId" added as blank. --- extensions/yii/authclient/clients/GoogleOpenId.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 extensions/yii/authclient/clients/GoogleOpenId.php diff --git a/extensions/yii/authclient/clients/GoogleOpenId.php b/extensions/yii/authclient/clients/GoogleOpenId.php new file mode 100644 index 0000000..7eca6d7 --- /dev/null +++ b/extensions/yii/authclient/clients/GoogleOpenId.php @@ -0,0 +1,21 @@ + + * @since 2.0 + */ +class GoogleOpenId extends OpenId +{ + +} \ No newline at end of file From 938b6c4e9e9ffc21216969a9a05f5acb93fc406b Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Thu, 19 Dec 2013 17:06:41 +0200 Subject: [PATCH 12/37] Google open id default params added. --- extensions/yii/authclient/AuthAction.php | 4 ++-- extensions/yii/authclient/OpenId.php | 5 ++--- extensions/yii/authclient/clients/GoogleOpenId.php | 12 +++++++++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/extensions/yii/authclient/AuthAction.php b/extensions/yii/authclient/AuthAction.php index 9ccd848..d8af5e2 100644 --- a/extensions/yii/authclient/AuthAction.php +++ b/extensions/yii/authclient/AuthAction.php @@ -25,7 +25,7 @@ class AuthAction extends Action /** * @var string name of the auth client collection application component. */ - public $clientCollection; + public $clientCollection = 'auth'; /** * @var string name of the GET param, which is used to passed auth client id to this action. */ @@ -227,7 +227,7 @@ class AuthAction extends Action break; } } else { - $provider->identity = $provider->authUrl; // Setting identifier + //$provider->identity = $provider->authUrl; // Setting identifier $request = Yii::$app->getRequest(); $provider->realm = $request->getHostInfo(); $provider->returnUrl = $provider->realm . $request->getUrl(); // getting return URL diff --git a/extensions/yii/authclient/OpenId.php b/extensions/yii/authclient/OpenId.php index b614578..46b2d9a 100644 --- a/extensions/yii/authclient/OpenId.php +++ b/extensions/yii/authclient/OpenId.php @@ -540,7 +540,7 @@ class OpenId extends Component implements ClientInterface $counts = []; $required = []; $optional = []; - foreach (['required', 'optional'] as $type) { + foreach (['requiredAttributes', 'optional'] as $type) { foreach ($this->$type as $alias => $field) { if (is_int($alias)) { $alias = strtr($field, '/', '_'); @@ -626,7 +626,6 @@ class OpenId extends Component implements ClientInterface $params['openid.identity'] = $this->identity; $params['openid.claimed_id'] = $this->claimed_id; } - return $this->buildUrl(parse_url($this->server), ['query' => http_build_query($params, '', '&')]); } @@ -747,7 +746,7 @@ class OpenId extends Component implements ClientInterface protected function getSregAttributes() { - $attributes = array(); + $attributes = []; $sregToAx = array_flip(self::$axToSregMap); foreach ($this->data as $key => $value) { $keyMatch = 'openid_sreg_'; diff --git a/extensions/yii/authclient/clients/GoogleOpenId.php b/extensions/yii/authclient/clients/GoogleOpenId.php index 7eca6d7..420f827 100644 --- a/extensions/yii/authclient/clients/GoogleOpenId.php +++ b/extensions/yii/authclient/clients/GoogleOpenId.php @@ -17,5 +17,15 @@ use yii\authclient\OpenId; */ class GoogleOpenId extends OpenId { - + public function init() + { + parent::init(); + $this->setIdentity('https://www.google.com/accounts/o8/id'); + $this->requiredAttributes = [ + 'namePerson/first', + 'namePerson/last', + 'contact/email', + 'pref/language', + ]; + } } \ No newline at end of file From 722c93fc4860b9104e81cf66110465700cd1dd49 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Fri, 20 Dec 2013 11:45:20 +0200 Subject: [PATCH 13/37] Yandex OpenId client added. --- extensions/yii/authclient/clients/YandexOpenId.php | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 extensions/yii/authclient/clients/YandexOpenId.php diff --git a/extensions/yii/authclient/clients/YandexOpenId.php b/extensions/yii/authclient/clients/YandexOpenId.php new file mode 100644 index 0000000..f2a856e --- /dev/null +++ b/extensions/yii/authclient/clients/YandexOpenId.php @@ -0,0 +1,29 @@ + + * @since 2.0 + */ +class YandexOpenId extends OpenId +{ + public function init() + { + parent::init(); + $this->setIdentity('http://openid.yandex.ru'); + $this->requiredAttributes = [ + 'namePerson', + 'contact/email', + ]; + } +} \ No newline at end of file From f06b5ab1611f29c82d176888b17a9c6d456bde1c Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Fri, 20 Dec 2013 14:04:26 +0200 Subject: [PATCH 14/37] Widget "yii\authclient\widgets\Choice" fixed. --- extensions/yii/authclient/ClientInterface.php | 5 ++ extensions/yii/authclient/ClientTrait.php | 35 +++++++++- extensions/yii/authclient/clients/GoogleOpenId.php | 14 ++++ extensions/yii/authclient/clients/YandexOpenId.php | 14 ++++ extensions/yii/authclient/widgets/Choice.php | 77 +++++++++++----------- .../unit/extensions/authclient/ClientTraitTest.php | 37 +++++++---- 6 files changed, 131 insertions(+), 51 deletions(-) diff --git a/extensions/yii/authclient/ClientInterface.php b/extensions/yii/authclient/ClientInterface.php index 2b08e1e..bd76e3b 100644 --- a/extensions/yii/authclient/ClientInterface.php +++ b/extensions/yii/authclient/ClientInterface.php @@ -49,4 +49,9 @@ interface ClientInterface * @return array list of user attributes */ public function getUserAttributes(); + + /** + * @return array view options in format: optionName => optionValue + */ + public function getViewOptions(); } \ No newline at end of file diff --git a/extensions/yii/authclient/ClientTrait.php b/extensions/yii/authclient/ClientTrait.php index cdd3150..6ce1574 100644 --- a/extensions/yii/authclient/ClientTrait.php +++ b/extensions/yii/authclient/ClientTrait.php @@ -39,6 +39,10 @@ trait ClientTrait * @var array authenticated user attributes. */ private $_userAttributes; + /** + * @var array view options in format: optionName => optionValue + */ + private $_viewOptions; /** * @param string $id service id. @@ -111,12 +115,31 @@ trait ClientTrait /** * @param array $userAttributes list of user attributes */ - public function setUserAttributes(array $userAttributes) + public function setUserAttributes($userAttributes) { $this->_userAttributes = $userAttributes; } /** + * @param array $viewOptions view options in format: optionName => optionValue + */ + public function setViewOptions($viewOptions) + { + $this->_viewOptions = $viewOptions; + } + + /** + * @return array view options in format: optionName => optionValue + */ + public function getViewOptions() + { + if ($this->_viewOptions === null) { + $this->_viewOptions = $this->defaultViewOptions(); + } + return $this->_viewOptions; + } + + /** * Generates service name. * @return string service name. */ @@ -142,4 +165,14 @@ trait ClientTrait { throw new NotSupportedException('Method "' . get_class($this) . '::' . __FUNCTION__ . '" not implemented.'); } + + /** + * Returns the default [[viewOptions]] value. + * Particular client may override this method in order to provide specific default view options. + * @return array list of default [[viewOptions]] + */ + protected function defaultViewOptions() + { + return []; + } } \ No newline at end of file diff --git a/extensions/yii/authclient/clients/GoogleOpenId.php b/extensions/yii/authclient/clients/GoogleOpenId.php index 420f827..a0d3e0f 100644 --- a/extensions/yii/authclient/clients/GoogleOpenId.php +++ b/extensions/yii/authclient/clients/GoogleOpenId.php @@ -17,6 +17,9 @@ use yii\authclient\OpenId; */ class GoogleOpenId extends OpenId { + /** + * @inheritdoc + */ public function init() { parent::init(); @@ -28,4 +31,15 @@ class GoogleOpenId extends OpenId 'pref/language', ]; } + + /** + * @inheritdoc + */ + protected function defaultViewOptions() + { + return [ + 'popupWidth' => 880, + 'popupHeight' => 520, + ]; + } } \ No newline at end of file diff --git a/extensions/yii/authclient/clients/YandexOpenId.php b/extensions/yii/authclient/clients/YandexOpenId.php index f2a856e..5ffc347 100644 --- a/extensions/yii/authclient/clients/YandexOpenId.php +++ b/extensions/yii/authclient/clients/YandexOpenId.php @@ -17,6 +17,9 @@ use yii\authclient\OpenId; */ class YandexOpenId extends OpenId { + /** + * @inheritdoc + */ public function init() { parent::init(); @@ -26,4 +29,15 @@ class YandexOpenId extends OpenId 'contact/email', ]; } + + /** + * @inheritdoc + */ + protected function defaultViewOptions() + { + return [ + 'popupWidth' => 900, + 'popupHeight' => 550, + ]; + } } \ No newline at end of file diff --git a/extensions/yii/authclient/widgets/Choice.php b/extensions/yii/authclient/widgets/Choice.php index 94b32ff..501aa01 100644 --- a/extensions/yii/authclient/widgets/Choice.php +++ b/extensions/yii/authclient/widgets/Choice.php @@ -10,12 +10,12 @@ namespace yii\authclient\widgets; use yii\base\Widget; use Yii; use yii\helpers\Html; -use yii\authclient\provider\ProviderInterface; +use yii\authclient\ClientInterface; /** * Class Choice * - * @property ProviderInterface[] $providers auth providers list. + * @property ClientInterface[] $providers auth providers list. * @property array $baseAuthUrl configuration for the external services base authentication URL. * * @author Paul Klimov @@ -24,28 +24,28 @@ use yii\authclient\provider\ProviderInterface; class Choice extends Widget { /** - * @var ProviderInterface[] auth providers list. + * @var ClientInterface[] auth providers list. */ - private $_providers; + private $_clients; /** - * @var string name of the auth provider collection application component. + * @var string name of the auth client collection application component. * This component will be used to fetch {@link services} value if it is not set. */ - public $providerCollection; + public $clientCollection = 'auth'; /** - * @var array configuration for the external services base authentication URL. + * @var array configuration for the external clients base authentication URL. */ private $_baseAuthUrl; /** - * @var string name of the GET param , which should be used to passed auth provider id to URL + * @var string name of the GET param , which should be used to passed auth client id to URL * defined by {@link baseAuthUrl}. */ - public $providerIdGetParamName = 'provider'; + public $clientIdGetParamName = 'client_id'; /** * @var array the HTML attributes that should be rendered in the div HTML tag representing the container element. */ public $mainContainerHtmlOptions = [ - 'class' => 'services' + 'class' => 'auth-clients' ]; /** * @var boolean indicates if popup window should be used instead of direct links. @@ -58,22 +58,22 @@ class Choice extends Widget public $autoRender = true; /** - * @param ProviderInterface[] $providers auth providers + * @param ClientInterface[] $clients auth providers */ - public function setProviders(array $providers) + public function setClients(array $clients) { - $this->_providers = $providers; + $this->_clients = $clients; } /** - * @return ProviderInterface[] auth providers + * @return ClientInterface[] auth providers */ - public function getProviders() + public function getClients() { - if ($this->_providers === null) { - $this->_providers = $this->defaultProviders(); + if ($this->_clients === null) { + $this->_clients = $this->defaultClients(); } - return $this->_providers; + return $this->_clients; } /** @@ -96,14 +96,14 @@ class Choice extends Widget } /** - * Returns default auth providers list. - * @return ProviderInterface[] auth providers list. + * Returns default auth clients list. + * @return ClientInterface[] auth clients list. */ - protected function defaultProviders() + protected function defaultClients() { - /** @var $collection \yii\authclient\provider\Collection */ - $collection = Yii::$app->getComponent($this->providerCollection); - return $collection->getProviders(); + /** @var $collection \yii\authclient\Collection */ + $collection = Yii::$app->getComponent($this->clientCollection); + return $collection->getClients(); } /** @@ -116,47 +116,48 @@ class Choice extends Widget Yii::$app->controller->getRoute() ]; $params = $_GET; - unset($params[$this->providerIdGetParamName]); + unset($params[$this->clientIdGetParamName]); $baseAuthUrl = array_merge($baseAuthUrl, $params); return $baseAuthUrl; } /** * Outputs external service auth link. - * @param ProviderInterface $service external auth service instance. + * @param ClientInterface $client external auth client instance. * @param string $text link text, if not set - default value will be generated. * @param array $htmlOptions link HTML options. */ - public function providerLink($service, $text = null, array $htmlOptions = []) + public function providerLink($client, $text = null, array $htmlOptions = []) { if ($text === null) { - $text = Html::tag('span', ['class' => 'auth-icon ' . $service->getName()], ''); - $text .= Html::tag('span', ['class' => 'auth-title'], $service->getTitle()); + $text = Html::tag('span', '', ['class' => 'auth-icon ' . $client->getName()]); + $text .= Html::tag('span', $client->getTitle(), ['class' => 'auth-title']); } if (!array_key_exists('class', $htmlOptions)) { - $htmlOptions['class'] = 'auth-link ' . $service->getName(); + $htmlOptions['class'] = 'auth-link ' . $client->getName(); } if ($this->popupMode) { - if (isset($service->popupWidth)) { - $htmlOptions['data-popup-width'] = $service->popupWidth; + $viewOptions = $client->getViewOptions(); + if (isset($viewOptions['popupWidth'])) { + $htmlOptions['data-popup-width'] = $viewOptions['popupWidth']; } - if (isset($service->popupHeight)) { - $htmlOptions['data-popup-height'] = $service->popupHeight; + if (isset($viewOptions['popupHeight'])) { + $htmlOptions['data-popup-height'] = $viewOptions['popupHeight']; } } - echo Html::a($text, $this->createProviderUrl($service), $htmlOptions); + echo Html::a($text, $this->createProviderUrl($client), $htmlOptions); } /** * Composes external service auth URL. - * @param ProviderInterface $provider external auth service instance. + * @param ClientInterface $provider external auth service instance. * @return string auth URL. */ public function createProviderUrl($provider) { $this->autoRender = false; $url = $this->getBaseAuthUrl(); - $url[$this->providerIdGetParamName] = $provider->getId(); + $url[$this->clientIdGetParamName] = $provider->getId(); return Html::url($url); } @@ -166,7 +167,7 @@ class Choice extends Widget protected function renderMainContent() { echo Html::beginTag('ul', ['class' => 'auth-services clear']); - foreach ($this->getProviders() as $externalService) { + foreach ($this->getClients() as $externalService) { echo Html::beginTag('li', ['class' => 'auth-service']); $this->providerLink($externalService); echo Html::endTag('li'); diff --git a/tests/unit/extensions/authclient/ClientTraitTest.php b/tests/unit/extensions/authclient/ClientTraitTest.php index 404533f..5f51a08 100644 --- a/tests/unit/extensions/authclient/ClientTraitTest.php +++ b/tests/unit/extensions/authclient/ClientTraitTest.php @@ -26,33 +26,46 @@ class ClientTraitTest extends TestCase public function testSetGet() { - $provider = new Client(); + $client = new Client(); $id = 'test_id'; - $provider->setId($id); - $this->assertEquals($id, $provider->getId(), 'Unable to setup id!'); + $client->setId($id); + $this->assertEquals($id, $client->getId(), 'Unable to setup id!'); $name = 'test_name'; - $provider->setName($name); - $this->assertEquals($name, $provider->getName(), 'Unable to setup name!'); + $client->setName($name); + $this->assertEquals($name, $client->getName(), 'Unable to setup name!'); $title = 'test_title'; - $provider->setTitle($title); - $this->assertEquals($title, $provider->getTitle(), 'Unable to setup title!'); + $client->setTitle($title); + $this->assertEquals($title, $client->getTitle(), 'Unable to setup title!'); + + $userAttributes = [ + 'attribute1' => 'value1', + 'attribute2' => 'value2', + ]; + $client->setUserAttributes($userAttributes); + $this->assertEquals($userAttributes, $client->getUserAttributes(), 'Unable to setup user attributes!'); + + $viewOptions = [ + 'option1' => 'value1', + 'option2' => 'value2', + ]; + $client->setViewOptions($viewOptions); + $this->assertEquals($viewOptions, $client->getViewOptions(), 'Unable to setup view options!'); } - public function testGetDescriptiveData() + public function testGetDefaults() { $provider = new Client(); - $this->assertNotEmpty($provider->getName(), 'Unable to get name!'); - $this->assertNotEmpty($provider->getTitle(), 'Unable to get title!'); + $this->assertNotEmpty($provider->getName(), 'Unable to get default name!'); + $this->assertNotEmpty($provider->getTitle(), 'Unable to get default title!'); + $this->assertNotNull($provider->getViewOptions(), 'Unable to get default view options!'); } } class Client extends Object implements ClientInterface { use ClientTrait; - - public function authenticate() {} } \ No newline at end of file From ba95e1dd12e08f949bd3a7b5c70520ae9928cd34 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Mon, 23 Dec 2013 11:12:14 +0200 Subject: [PATCH 15/37] Normalization of user attributes added to Auth Client. --- extensions/yii/authclient/ClientTrait.php | 85 ++++++++++++++++++---- .../unit/extensions/authclient/ClientTraitTest.php | 54 +++++++++----- 2 files changed, 105 insertions(+), 34 deletions(-) diff --git a/extensions/yii/authclient/ClientTrait.php b/extensions/yii/authclient/ClientTrait.php index 6ce1574..d79899c 100644 --- a/extensions/yii/authclient/ClientTrait.php +++ b/extensions/yii/authclient/ClientTrait.php @@ -12,27 +12,35 @@ use yii\base\NotSupportedException; use yii\helpers\StringHelper; /** - * Class ProviderTrait + * ProviderTrait can be used to satisfy [[ClientInterface]] interface. * * @see ClientInterface * + * @property string $id auth service id. + * @property string $name auth service name. + * @property string $title auth service title. + * @property array $userAttributes authenticated user attributes. + * @property array $normalizeUserAttributeMap map used to normalize user attributes fetched from + * external auth service in format: rawAttributeName => normalizedAttributeName. + * @property array $viewOptions view options in format: optionName => optionValue. + * * @author Paul Klimov * @since 2.0 */ trait ClientTrait { /** - * @var string service id. + * @var string auth service id. * This value mainly used as HTTP request parameter. */ private $_id; /** - * @var string service unique name. + * @var string auth service name. * This value may be used in database records, CSS files and so on. */ private $_name; /** - * @var string service title to display in views. + * @var string auth service title to display in views. */ private $_title; /** @@ -40,6 +48,11 @@ trait ClientTrait */ private $_userAttributes; /** + * @var array map used to normalize user attributes fetched from external auth service + * in format: rawAttributeName => normalizedAttributeName + */ + private $_normalizeUserAttributeMap; + /** * @var array view options in format: optionName => optionValue */ private $_viewOptions; @@ -64,6 +77,14 @@ trait ClientTrait } /** + * @param string $name service name. + */ + public function setName($name) + { + $this->_name = $name; + } + + /** * @return string service name. */ public function getName() @@ -75,11 +96,11 @@ trait ClientTrait } /** - * @param string $name service name. + * @param string $title service title. */ - public function setName($name) + public function setTitle($title) { - $this->_name = $name; + $this->_title = $title; } /** @@ -94,11 +115,11 @@ trait ClientTrait } /** - * @param string $title service title. + * @param array $userAttributes list of user attributes */ - public function setTitle($title) + public function setUserAttributes($userAttributes) { - $this->_title = $title; + $this->_userAttributes = $this->normalizeUserAttributes($userAttributes); } /** @@ -107,17 +128,28 @@ trait ClientTrait public function getUserAttributes() { if ($this->_userAttributes === null) { - $this->_userAttributes = $this->initUserAttributes(); + $this->_userAttributes = $this->normalizeUserAttributes($this->initUserAttributes()); } return $this->_userAttributes; } /** - * @param array $userAttributes list of user attributes + * @param array $normalizeUserAttributeMap normalize user attribute map. */ - public function setUserAttributes($userAttributes) + public function setNormalizeUserAttributeMap($normalizeUserAttributeMap) { - $this->_userAttributes = $userAttributes; + $this->_normalizeUserAttributeMap = $normalizeUserAttributeMap; + } + + /** + * @return array normalize user attribute map. + */ + public function getNormalizeUserAttributeMap() + { + if ($this->_normalizeUserAttributeMap === null) { + $this->_normalizeUserAttributeMap = $this->defaultNormalizeUserAttributeMap(); + } + return $this->_normalizeUserAttributeMap; } /** @@ -167,6 +199,16 @@ trait ClientTrait } /** + * Returns the default [[normalizeUserAttributeMap]] value. + * Particular client may override this method in order to provide specific default map. + * @return array normalize attribute map. + */ + public function defaultNormalizeUserAttributeMap() + { + return []; + } + + /** * Returns the default [[viewOptions]] value. * Particular client may override this method in order to provide specific default view options. * @return array list of default [[viewOptions]] @@ -175,4 +217,19 @@ trait ClientTrait { return []; } + + /** + * Normalize given user attributes according to {@link normalizeUserAttributeMap}. + * @param array $attributes raw attributes. + * @return array normalized attributes. + */ + protected function normalizeUserAttributes($attributes) + { + foreach ($this->getNormalizeUserAttributeMap() as $normalizedName => $actualName) { + if (array_key_exists($actualName, $attributes)) { + $attributes[$normalizedName] = $attributes[$actualName]; + } + } + return $attributes; + } } \ No newline at end of file diff --git a/tests/unit/extensions/authclient/ClientTraitTest.php b/tests/unit/extensions/authclient/ClientTraitTest.php index 5f51a08..76a3fdd 100644 --- a/tests/unit/extensions/authclient/ClientTraitTest.php +++ b/tests/unit/extensions/authclient/ClientTraitTest.php @@ -8,22 +8,6 @@ use yii\base\Object; class ClientTraitTest extends TestCase { - protected function setUp() - { - $config = [ - 'components' => [ - 'user' => [ - 'identityClass' => '\yii\web\IdentityInterface' - ], - 'request' => [ - 'hostInfo' => 'http://testdomain.com', - 'scriptUrl' => '/index.php', - ], - ] - ]; - $this->mockApplication($config, '\yii\web\Application'); - } - public function testSetGet() { $client = new Client(); @@ -47,6 +31,13 @@ class ClientTraitTest extends TestCase $client->setUserAttributes($userAttributes); $this->assertEquals($userAttributes, $client->getUserAttributes(), 'Unable to setup user attributes!'); + $normalizeUserAttributeMap = [ + 'name' => 'some/name', + 'email' => 'some/email', + ]; + $client->setNormalizeUserAttributeMap($normalizeUserAttributeMap); + $this->assertEquals($normalizeUserAttributeMap, $client->getNormalizeUserAttributeMap(), 'Unable to setup normalize user attribute map!'); + $viewOptions = [ 'option1' => 'value1', 'option2' => 'value2', @@ -57,11 +48,34 @@ class ClientTraitTest extends TestCase public function testGetDefaults() { - $provider = new Client(); + $client = new Client(); + + $this->assertNotEmpty($client->getName(), 'Unable to get default name!'); + $this->assertNotEmpty($client->getTitle(), 'Unable to get default title!'); + $this->assertNotNull($client->getViewOptions(), 'Unable to get default view options!'); + $this->assertNotNull($client->getNormalizeUserAttributeMap(), 'Unable to get default normalize user attribute map!'); + } + + /** + * @depends testSetGet + */ + public function testNormalizeUserAttributes() + { + $client = new Client(); - $this->assertNotEmpty($provider->getName(), 'Unable to get default name!'); - $this->assertNotEmpty($provider->getTitle(), 'Unable to get default title!'); - $this->assertNotNull($provider->getViewOptions(), 'Unable to get default view options!'); + $normalizeUserAttributeMap = [ + 'raw/name' => 'name', + 'raw/email' => 'email', + ]; + $client->setNormalizeUserAttributeMap($normalizeUserAttributeMap); + $rawUserAttributes = [ + 'raw/name' => 'name value', + 'raw/email' => 'email value', + ]; + $client->setUserAttributes($rawUserAttributes); + $normalizedUserAttributes = $client->getUserAttributes(); + $expectedNormalizedUserAttributes = array_combine(array_keys($normalizeUserAttributeMap), array_values($rawUserAttributes)); + $this->assertEquals($expectedNormalizedUserAttributes, $normalizedUserAttributes); } } From a286f3095144fe81bb8e5db350f693b85034f0e8 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Mon, 23 Dec 2013 11:18:43 +0200 Subject: [PATCH 16/37] Default normalize user attribute map added to OpentId clients. --- extensions/yii/authclient/ClientTrait.php | 2 +- extensions/yii/authclient/clients/GoogleOpenId.php | 13 +++++++++++++ extensions/yii/authclient/clients/YandexOpenId.php | 11 +++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/extensions/yii/authclient/ClientTrait.php b/extensions/yii/authclient/ClientTrait.php index d79899c..016eb7b 100644 --- a/extensions/yii/authclient/ClientTrait.php +++ b/extensions/yii/authclient/ClientTrait.php @@ -203,7 +203,7 @@ trait ClientTrait * Particular client may override this method in order to provide specific default map. * @return array normalize attribute map. */ - public function defaultNormalizeUserAttributeMap() + protected function defaultNormalizeUserAttributeMap() { return []; } diff --git a/extensions/yii/authclient/clients/GoogleOpenId.php b/extensions/yii/authclient/clients/GoogleOpenId.php index a0d3e0f..aa67327 100644 --- a/extensions/yii/authclient/clients/GoogleOpenId.php +++ b/extensions/yii/authclient/clients/GoogleOpenId.php @@ -35,6 +35,19 @@ class GoogleOpenId extends OpenId /** * @inheritdoc */ + protected function defaultNormalizeUserAttributeMap() + { + return [ + 'first_name' => 'namePerson/first', + 'last_name' => 'namePerson/last', + 'email' => 'contact/email', + 'language' => 'pref/language', + ]; + } + + /** + * @inheritdoc + */ protected function defaultViewOptions() { return [ diff --git a/extensions/yii/authclient/clients/YandexOpenId.php b/extensions/yii/authclient/clients/YandexOpenId.php index 5ffc347..24c5338 100644 --- a/extensions/yii/authclient/clients/YandexOpenId.php +++ b/extensions/yii/authclient/clients/YandexOpenId.php @@ -33,6 +33,17 @@ class YandexOpenId extends OpenId /** * @inheritdoc */ + protected function defaultNormalizeUserAttributeMap() + { + return [ + 'name' => 'namePerson', + 'email' => 'contact/email', + ]; + } + + /** + * @inheritdoc + */ protected function defaultViewOptions() { return [ From 3c8c85514574f4bdd3c658a6b3c983b40fd3c833 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Mon, 23 Dec 2013 12:07:33 +0200 Subject: [PATCH 17/37] 'GoogleOAuth' auth client added. --- extensions/yii/authclient/AuthAction.php | 23 +++++----- extensions/yii/authclient/clients/GoogleOAuth.php | 54 +++++++++++++++++++++++ extensions/yii/authclient/widgets/Choice.php | 2 +- 3 files changed, 67 insertions(+), 12 deletions(-) create mode 100644 extensions/yii/authclient/clients/GoogleOAuth.php diff --git a/extensions/yii/authclient/AuthAction.php b/extensions/yii/authclient/AuthAction.php index d8af5e2..a7e14cf 100644 --- a/extensions/yii/authclient/AuthAction.php +++ b/extensions/yii/authclient/AuthAction.php @@ -29,7 +29,7 @@ class AuthAction extends Action /** * @var string name of the GET param, which is used to passed auth client id to this action. */ - public $clientIdGetParamName = 'client_id'; + public $clientIdGetParamName = 'authclient'; /** * @var callable PHP callback, which should be triggered in case of successful authentication. */ @@ -119,19 +119,20 @@ class AuthAction extends Action } /** - * @param mixed $provider - * @throws \yii\base\NotSupportedException + * @param mixed $client auth client instance. + * @return \yii\web\Response response instance. + * @throws \yii\base\NotSupportedException on invalid client. */ - protected function auth($provider) + protected function auth($client) { - if ($provider instanceof OpenId) { - return $this->authOpenId($provider); - } elseif ($provider instanceof OAuth2) { - return $this->authOAuth2($provider); - } elseif ($provider instanceof OAuth1) { - return $this->authOAuth1($provider); + if ($client instanceof OpenId) { + return $this->authOpenId($client); + } elseif ($client instanceof OAuth2) { + return $this->authOAuth2($client); + } elseif ($client instanceof OAuth1) { + return $this->authOAuth1($client); } else { - throw new NotSupportedException('Provider "' . get_class($provider) . '" is not supported.'); + throw new NotSupportedException('Provider "' . get_class($client) . '" is not supported.'); } } diff --git a/extensions/yii/authclient/clients/GoogleOAuth.php b/extensions/yii/authclient/clients/GoogleOAuth.php new file mode 100644 index 0000000..fa56b7f --- /dev/null +++ b/extensions/yii/authclient/clients/GoogleOAuth.php @@ -0,0 +1,54 @@ + + * @since 2.0 + */ +class GoogleOAuth extends OAuth2 +{ + /** + * @inheritdoc + */ + public function __construct($config = []) + { + $config = array_merge( + [ + 'clientId' => 'anonymous', + 'clientSecret' => 'anonymous', + 'authUrl' => 'https://accounts.google.com/o/oauth2/auth', + 'tokenUrl' => 'https://accounts.google.com/o/oauth2/token', + 'apiBaseUrl' => 'https://www.googleapis.com/oauth2/v1', + 'scope' => implode(' ', [ + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/userinfo.email', + ]), + ], + $config + ); + parent::__construct($config); + } + + /** + * @inheritdoc + */ + protected function initUserAttributes() + { + $attributes = $this->api('userinfo', 'GET'); + return $attributes; + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/widgets/Choice.php b/extensions/yii/authclient/widgets/Choice.php index 501aa01..3cf2247 100644 --- a/extensions/yii/authclient/widgets/Choice.php +++ b/extensions/yii/authclient/widgets/Choice.php @@ -40,7 +40,7 @@ class Choice extends Widget * @var string name of the GET param , which should be used to passed auth client id to URL * defined by {@link baseAuthUrl}. */ - public $clientIdGetParamName = 'client_id'; + public $clientIdGetParamName = 'authclient'; /** * @var array the HTML attributes that should be rendered in the div HTML tag representing the container element. */ From 24d4f4b908e360c4366b2e5be19519abcd1c05a1 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Mon, 23 Dec 2013 13:04:22 +0200 Subject: [PATCH 18/37] Signature base string generation for the OAuth1 fixed. --- extensions/yii/authclient/OAuth1.php | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/yii/authclient/OAuth1.php b/extensions/yii/authclient/OAuth1.php index 11c99e8..e68118d 100644 --- a/extensions/yii/authclient/OAuth1.php +++ b/extensions/yii/authclient/OAuth1.php @@ -298,6 +298,7 @@ class OAuth1 extends BaseOAuth protected function composeSignatureBaseString($method, $url, array $params) { unset($params['oauth_signature']); + uksort($params, 'strcmp'); // Parameters are sorted by name, using lexicographical byte value ordering. Ref: Spec: 9.1.1 $parts = [ strtoupper($method), $url, From 2e66467cadf1ff8a961db53e4c15d9b8af0ebd65 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Mon, 23 Dec 2013 13:31:33 +0200 Subject: [PATCH 19/37] OAuth url-encoded response processing fixed. --- extensions/yii/authclient/BaseOAuth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/yii/authclient/BaseOAuth.php b/extensions/yii/authclient/BaseOAuth.php index 4c0a025..2a6b6b3 100644 --- a/extensions/yii/authclient/BaseOAuth.php +++ b/extensions/yii/authclient/BaseOAuth.php @@ -263,7 +263,7 @@ abstract class BaseOAuth extends Component implements ClientInterface } case self::CONTENT_TYPE_URLENCODED: { $response = []; - parse_url($rawResponse, $response); + parse_str($rawResponse, $response); break; } case self::CONTENT_TYPE_XML: { From 8abd201b3c39a500aba0d76373eb614431450a83 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Mon, 23 Dec 2013 13:31:49 +0200 Subject: [PATCH 20/37] Twitter auth client added. --- extensions/yii/authclient/clients/Twitter.php | 53 +++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 extensions/yii/authclient/clients/Twitter.php diff --git a/extensions/yii/authclient/clients/Twitter.php b/extensions/yii/authclient/clients/Twitter.php new file mode 100644 index 0000000..4add039 --- /dev/null +++ b/extensions/yii/authclient/clients/Twitter.php @@ -0,0 +1,53 @@ + + * @since 2.0 + */ +class Twitter extends OAuth1 +{ + /** + * @inheritdoc + */ + public function __construct($config = []) + { + $config = array_merge( + [ + 'consumerKey' => 'anonymous', + 'consumerSecret' => 'anonymous', + 'requestTokenUrl' => 'https://api.twitter.com/oauth/request_token', + 'requestTokenMethod' => 'POST', + 'accessTokenUrl' => 'https://api.twitter.com/oauth/access_token', + 'accessTokenMethod' => 'POST', + 'authUrl' => 'https://api.twitter.com/oauth/authorize', + 'scope' => '', + 'apiBaseUrl' => 'https://api.twitter.com/1.1', + ], + $config + ); + parent::__construct($config); + } + + /** + * @inheritdoc + */ + protected function initUserAttributes() + { + return $this->api('account/verify_credentials.json', 'GET'); + } +} \ No newline at end of file From 7a4c1cd3872c7e8a9a4a696721eece1ae7357065 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Mon, 23 Dec 2013 15:31:23 +0200 Subject: [PATCH 21/37] Trait "ClientTrait" converted into "BaseClient" class. --- extensions/yii/authclient/BaseClient.php | 236 +++++++++++++++++++++ extensions/yii/authclient/BaseOAuth.php | 5 +- extensions/yii/authclient/ClientTrait.php | 235 -------------------- extensions/yii/authclient/OpenId.php | 5 +- .../unit/extensions/authclient/BaseClientTest.php | 82 +++++++ .../unit/extensions/authclient/ClientTraitTest.php | 85 -------- .../unit/extensions/authclient/CollectionTest.php | 9 +- 7 files changed, 322 insertions(+), 335 deletions(-) create mode 100644 extensions/yii/authclient/BaseClient.php delete mode 100644 extensions/yii/authclient/ClientTrait.php create mode 100644 tests/unit/extensions/authclient/BaseClientTest.php delete mode 100644 tests/unit/extensions/authclient/ClientTraitTest.php diff --git a/extensions/yii/authclient/BaseClient.php b/extensions/yii/authclient/BaseClient.php new file mode 100644 index 0000000..2741c2e --- /dev/null +++ b/extensions/yii/authclient/BaseClient.php @@ -0,0 +1,236 @@ + normalizedAttributeName. + * @property array $viewOptions view options in format: optionName => optionValue. + * + * @author Paul Klimov + * @since 2.0 + */ +abstract class BaseClient extends Component implements ClientInterface +{ + /** + * @var string auth service id. + * This value mainly used as HTTP request parameter. + */ + private $_id; + /** + * @var string auth service name. + * This value may be used in database records, CSS files and so on. + */ + private $_name; + /** + * @var string auth service title to display in views. + */ + private $_title; + /** + * @var array authenticated user attributes. + */ + private $_userAttributes; + /** + * @var array map used to normalize user attributes fetched from external auth service + * in format: rawAttributeName => normalizedAttributeName + */ + private $_normalizeUserAttributeMap; + /** + * @var array view options in format: optionName => optionValue + */ + private $_viewOptions; + + /** + * @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; + } + + /** + * @param string $name service name. + */ + public function setName($name) + { + $this->_name = $name; + } + + /** + * @return string service name. + */ + public function getName() + { + if ($this->_name === null) { + $this->_name = $this->defaultName(); + } + return $this->_name; + } + + /** + * @param string $title service title. + */ + public function setTitle($title) + { + $this->_title = $title; + } + + /** + * @return string service title. + */ + public function getTitle() + { + if ($this->_title === null) { + $this->_title = $this->defaultTitle(); + } + return $this->_title; + } + + /** + * @param array $userAttributes list of user attributes + */ + public function setUserAttributes($userAttributes) + { + $this->_userAttributes = $this->normalizeUserAttributes($userAttributes); + } + + /** + * @return array list of user attributes + */ + public function getUserAttributes() + { + if ($this->_userAttributes === null) { + $this->_userAttributes = $this->normalizeUserAttributes($this->initUserAttributes()); + } + return $this->_userAttributes; + } + + /** + * @param array $normalizeUserAttributeMap normalize user attribute map. + */ + public function setNormalizeUserAttributeMap($normalizeUserAttributeMap) + { + $this->_normalizeUserAttributeMap = $normalizeUserAttributeMap; + } + + /** + * @return array normalize user attribute map. + */ + public function getNormalizeUserAttributeMap() + { + if ($this->_normalizeUserAttributeMap === null) { + $this->_normalizeUserAttributeMap = $this->defaultNormalizeUserAttributeMap(); + } + return $this->_normalizeUserAttributeMap; + } + + /** + * @param array $viewOptions view options in format: optionName => optionValue + */ + public function setViewOptions($viewOptions) + { + $this->_viewOptions = $viewOptions; + } + + /** + * @return array view options in format: optionName => optionValue + */ + public function getViewOptions() + { + if ($this->_viewOptions === null) { + $this->_viewOptions = $this->defaultViewOptions(); + } + return $this->_viewOptions; + } + + /** + * 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)); + } + + /** + * Initializes authenticated user attributes. + * @return array auth user attributes. + */ + protected function initUserAttributes() + { + throw new NotSupportedException('Method "' . get_class($this) . '::' . __FUNCTION__ . '" not implemented.'); + } + + /** + * Returns the default [[normalizeUserAttributeMap]] value. + * Particular client may override this method in order to provide specific default map. + * @return array normalize attribute map. + */ + protected function defaultNormalizeUserAttributeMap() + { + return []; + } + + /** + * Returns the default [[viewOptions]] value. + * Particular client may override this method in order to provide specific default view options. + * @return array list of default [[viewOptions]] + */ + protected function defaultViewOptions() + { + return []; + } + + /** + * Normalize given user attributes according to {@link normalizeUserAttributeMap}. + * @param array $attributes raw attributes. + * @return array normalized attributes. + */ + protected function normalizeUserAttributes($attributes) + { + foreach ($this->getNormalizeUserAttributeMap() as $normalizedName => $actualName) { + if (array_key_exists($actualName, $attributes)) { + $attributes[$normalizedName] = $attributes[$actualName]; + } + } + return $attributes; + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/BaseOAuth.php b/extensions/yii/authclient/BaseOAuth.php index 2a6b6b3..c30ce1c 100644 --- a/extensions/yii/authclient/BaseOAuth.php +++ b/extensions/yii/authclient/BaseOAuth.php @@ -7,7 +7,6 @@ namespace yii\authclient; -use yii\base\Component; use yii\base\Exception; use yii\base\InvalidParamException; use Yii; @@ -21,10 +20,8 @@ use yii\helpers\Json; * @author Paul Klimov * @since 2.0 */ -abstract class BaseOAuth extends Component implements ClientInterface +abstract class BaseOAuth extends BaseClient implements ClientInterface { - use ClientTrait; - 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 diff --git a/extensions/yii/authclient/ClientTrait.php b/extensions/yii/authclient/ClientTrait.php deleted file mode 100644 index 016eb7b..0000000 --- a/extensions/yii/authclient/ClientTrait.php +++ /dev/null @@ -1,235 +0,0 @@ - normalizedAttributeName. - * @property array $viewOptions view options in format: optionName => optionValue. - * - * @author Paul Klimov - * @since 2.0 - */ -trait ClientTrait -{ - /** - * @var string auth service id. - * This value mainly used as HTTP request parameter. - */ - private $_id; - /** - * @var string auth service name. - * This value may be used in database records, CSS files and so on. - */ - private $_name; - /** - * @var string auth service title to display in views. - */ - private $_title; - /** - * @var array authenticated user attributes. - */ - private $_userAttributes; - /** - * @var array map used to normalize user attributes fetched from external auth service - * in format: rawAttributeName => normalizedAttributeName - */ - private $_normalizeUserAttributeMap; - /** - * @var array view options in format: optionName => optionValue - */ - private $_viewOptions; - - /** - * @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; - } - - /** - * @param string $name service name. - */ - public function setName($name) - { - $this->_name = $name; - } - - /** - * @return string service name. - */ - public function getName() - { - if ($this->_name === null) { - $this->_name = $this->defaultName(); - } - return $this->_name; - } - - /** - * @param string $title service title. - */ - public function setTitle($title) - { - $this->_title = $title; - } - - /** - * @return string service title. - */ - public function getTitle() - { - if ($this->_title === null) { - $this->_title = $this->defaultTitle(); - } - return $this->_title; - } - - /** - * @param array $userAttributes list of user attributes - */ - public function setUserAttributes($userAttributes) - { - $this->_userAttributes = $this->normalizeUserAttributes($userAttributes); - } - - /** - * @return array list of user attributes - */ - public function getUserAttributes() - { - if ($this->_userAttributes === null) { - $this->_userAttributes = $this->normalizeUserAttributes($this->initUserAttributes()); - } - return $this->_userAttributes; - } - - /** - * @param array $normalizeUserAttributeMap normalize user attribute map. - */ - public function setNormalizeUserAttributeMap($normalizeUserAttributeMap) - { - $this->_normalizeUserAttributeMap = $normalizeUserAttributeMap; - } - - /** - * @return array normalize user attribute map. - */ - public function getNormalizeUserAttributeMap() - { - if ($this->_normalizeUserAttributeMap === null) { - $this->_normalizeUserAttributeMap = $this->defaultNormalizeUserAttributeMap(); - } - return $this->_normalizeUserAttributeMap; - } - - /** - * @param array $viewOptions view options in format: optionName => optionValue - */ - public function setViewOptions($viewOptions) - { - $this->_viewOptions = $viewOptions; - } - - /** - * @return array view options in format: optionName => optionValue - */ - public function getViewOptions() - { - if ($this->_viewOptions === null) { - $this->_viewOptions = $this->defaultViewOptions(); - } - return $this->_viewOptions; - } - - /** - * 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)); - } - - /** - * Initializes authenticated user attributes. - * @return array auth user attributes. - */ - protected function initUserAttributes() - { - throw new NotSupportedException('Method "' . get_class($this) . '::' . __FUNCTION__ . '" not implemented.'); - } - - /** - * Returns the default [[normalizeUserAttributeMap]] value. - * Particular client may override this method in order to provide specific default map. - * @return array normalize attribute map. - */ - protected function defaultNormalizeUserAttributeMap() - { - return []; - } - - /** - * Returns the default [[viewOptions]] value. - * Particular client may override this method in order to provide specific default view options. - * @return array list of default [[viewOptions]] - */ - protected function defaultViewOptions() - { - return []; - } - - /** - * Normalize given user attributes according to {@link normalizeUserAttributeMap}. - * @param array $attributes raw attributes. - * @return array normalized attributes. - */ - protected function normalizeUserAttributes($attributes) - { - foreach ($this->getNormalizeUserAttributeMap() as $normalizedName => $actualName) { - if (array_key_exists($actualName, $attributes)) { - $attributes[$normalizedName] = $attributes[$actualName]; - } - } - return $attributes; - } -} \ No newline at end of file diff --git a/extensions/yii/authclient/OpenId.php b/extensions/yii/authclient/OpenId.php index 46b2d9a..0665ceb 100644 --- a/extensions/yii/authclient/OpenId.php +++ b/extensions/yii/authclient/OpenId.php @@ -7,7 +7,6 @@ namespace yii\authclient; -use yii\base\Component; use yii\base\Exception; use yii\base\NotSupportedException; @@ -25,10 +24,8 @@ use yii\base\NotSupportedException; * @author Paul Klimov * @since 2.0 */ -class OpenId extends Component implements ClientInterface +class OpenId extends BaseClient implements ClientInterface { - use ClientTrait; - /** * @var array list of attributes, which should be requested from server. */ diff --git a/tests/unit/extensions/authclient/BaseClientTest.php b/tests/unit/extensions/authclient/BaseClientTest.php new file mode 100644 index 0000000..462d5c2 --- /dev/null +++ b/tests/unit/extensions/authclient/BaseClientTest.php @@ -0,0 +1,82 @@ +setId($id); + $this->assertEquals($id, $client->getId(), 'Unable to setup id!'); + + $name = 'test_name'; + $client->setName($name); + $this->assertEquals($name, $client->getName(), 'Unable to setup name!'); + + $title = 'test_title'; + $client->setTitle($title); + $this->assertEquals($title, $client->getTitle(), 'Unable to setup title!'); + + $userAttributes = [ + 'attribute1' => 'value1', + 'attribute2' => 'value2', + ]; + $client->setUserAttributes($userAttributes); + $this->assertEquals($userAttributes, $client->getUserAttributes(), 'Unable to setup user attributes!'); + + $normalizeUserAttributeMap = [ + 'name' => 'some/name', + 'email' => 'some/email', + ]; + $client->setNormalizeUserAttributeMap($normalizeUserAttributeMap); + $this->assertEquals($normalizeUserAttributeMap, $client->getNormalizeUserAttributeMap(), 'Unable to setup normalize user attribute map!'); + + $viewOptions = [ + 'option1' => 'value1', + 'option2' => 'value2', + ]; + $client->setViewOptions($viewOptions); + $this->assertEquals($viewOptions, $client->getViewOptions(), 'Unable to setup view options!'); + } + + public function testGetDefaults() + { + $client = new Client(); + + $this->assertNotEmpty($client->getName(), 'Unable to get default name!'); + $this->assertNotEmpty($client->getTitle(), 'Unable to get default title!'); + $this->assertNotNull($client->getViewOptions(), 'Unable to get default view options!'); + $this->assertNotNull($client->getNormalizeUserAttributeMap(), 'Unable to get default normalize user attribute map!'); + } + + /** + * @depends testSetGet + */ + public function testNormalizeUserAttributes() + { + $client = new Client(); + + $normalizeUserAttributeMap = [ + 'raw/name' => 'name', + 'raw/email' => 'email', + ]; + $client->setNormalizeUserAttributeMap($normalizeUserAttributeMap); + $rawUserAttributes = [ + 'raw/name' => 'name value', + 'raw/email' => 'email value', + ]; + $client->setUserAttributes($rawUserAttributes); + $normalizedUserAttributes = $client->getUserAttributes(); + $expectedNormalizedUserAttributes = array_combine(array_keys($normalizeUserAttributeMap), array_values($rawUserAttributes)); + $this->assertEquals($expectedNormalizedUserAttributes, $normalizedUserAttributes); + } +} + +class Client extends BaseClient +{ +} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/ClientTraitTest.php b/tests/unit/extensions/authclient/ClientTraitTest.php deleted file mode 100644 index 76a3fdd..0000000 --- a/tests/unit/extensions/authclient/ClientTraitTest.php +++ /dev/null @@ -1,85 +0,0 @@ -setId($id); - $this->assertEquals($id, $client->getId(), 'Unable to setup id!'); - - $name = 'test_name'; - $client->setName($name); - $this->assertEquals($name, $client->getName(), 'Unable to setup name!'); - - $title = 'test_title'; - $client->setTitle($title); - $this->assertEquals($title, $client->getTitle(), 'Unable to setup title!'); - - $userAttributes = [ - 'attribute1' => 'value1', - 'attribute2' => 'value2', - ]; - $client->setUserAttributes($userAttributes); - $this->assertEquals($userAttributes, $client->getUserAttributes(), 'Unable to setup user attributes!'); - - $normalizeUserAttributeMap = [ - 'name' => 'some/name', - 'email' => 'some/email', - ]; - $client->setNormalizeUserAttributeMap($normalizeUserAttributeMap); - $this->assertEquals($normalizeUserAttributeMap, $client->getNormalizeUserAttributeMap(), 'Unable to setup normalize user attribute map!'); - - $viewOptions = [ - 'option1' => 'value1', - 'option2' => 'value2', - ]; - $client->setViewOptions($viewOptions); - $this->assertEquals($viewOptions, $client->getViewOptions(), 'Unable to setup view options!'); - } - - public function testGetDefaults() - { - $client = new Client(); - - $this->assertNotEmpty($client->getName(), 'Unable to get default name!'); - $this->assertNotEmpty($client->getTitle(), 'Unable to get default title!'); - $this->assertNotNull($client->getViewOptions(), 'Unable to get default view options!'); - $this->assertNotNull($client->getNormalizeUserAttributeMap(), 'Unable to get default normalize user attribute map!'); - } - - /** - * @depends testSetGet - */ - public function testNormalizeUserAttributes() - { - $client = new Client(); - - $normalizeUserAttributeMap = [ - 'raw/name' => 'name', - 'raw/email' => 'email', - ]; - $client->setNormalizeUserAttributeMap($normalizeUserAttributeMap); - $rawUserAttributes = [ - 'raw/name' => 'name value', - 'raw/email' => 'email value', - ]; - $client->setUserAttributes($rawUserAttributes); - $normalizedUserAttributes = $client->getUserAttributes(); - $expectedNormalizedUserAttributes = array_combine(array_keys($normalizeUserAttributeMap), array_values($rawUserAttributes)); - $this->assertEquals($expectedNormalizedUserAttributes, $normalizedUserAttributes); - } -} - -class Client extends Object implements ClientInterface -{ - use ClientTrait; -} \ No newline at end of file diff --git a/tests/unit/extensions/authclient/CollectionTest.php b/tests/unit/extensions/authclient/CollectionTest.php index 14f65a0..3e0f5d0 100644 --- a/tests/unit/extensions/authclient/CollectionTest.php +++ b/tests/unit/extensions/authclient/CollectionTest.php @@ -3,9 +3,7 @@ namespace yiiunit\extensions\authclient; use yii\authclient\Collection; -use yii\authclient\ClientInterface; -use yii\authclient\ClientTrait; -use yii\base\Object; +use yii\authclient\BaseClient; use yiiunit\extensions\authclient\TestCase; class CollectionTest extends TestCase @@ -82,9 +80,6 @@ class CollectionTest extends TestCase } } -class TestClient extends Object implements ClientInterface +class TestClient extends BaseClient { - use ClientTrait; - - public function authenticate() {} } \ No newline at end of file From d6f35f07ae462c571f91058fb98e670b8bff9d31 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Mon, 23 Dec 2013 16:23:56 +0200 Subject: [PATCH 22/37] OAuth classes refactored. --- extensions/yii/authclient/BaseOAuth.php | 12 +++---- extensions/yii/authclient/OAuth1.php | 10 +++--- extensions/yii/authclient/OAuth2.php | 6 ++-- extensions/yii/authclient/clients/GoogleOAuth.php | 36 +++++++++++---------- extensions/yii/authclient/clients/Twitter.php | 39 ++++++++++++----------- 5 files changed, 55 insertions(+), 48 deletions(-) diff --git a/extensions/yii/authclient/BaseOAuth.php b/extensions/yii/authclient/BaseOAuth.php index c30ce1c..e23f36e 100644 --- a/extensions/yii/authclient/BaseOAuth.php +++ b/extensions/yii/authclient/BaseOAuth.php @@ -36,19 +36,19 @@ abstract class BaseOAuth extends BaseClient implements ClientInterface * Note: this should be absolute URL (with http:// or https:// leading). * By default current URL will be used. */ - private $_returnUrl = ''; + private $_returnUrl; /** * @var string API base URL. */ - public $apiBaseUrl = ''; + public $apiBaseUrl; /** * @var string authorize URL. */ - public $authUrl = ''; + public $authUrl; /** * @var string auth request scope. */ - public $scope = ''; + public $scope; /** * @var array cURL request options. Option values from this field will overwrite corresponding * values from {@link defaultCurlOptions()}. @@ -57,7 +57,7 @@ abstract class BaseOAuth extends BaseClient implements ClientInterface /** * @var OAuthToken|array access token instance or its array configuration. */ - private $_accessToken = null; + private $_accessToken; /** * @var signature\BaseMethod|array signature method instance or its array configuration. */ @@ -76,7 +76,7 @@ abstract class BaseOAuth extends BaseClient implements ClientInterface */ public function getReturnUrl() { - if (empty($this->_returnUrl)) { + if ($this->_returnUrl === null) { $this->_returnUrl = $this->defaultReturnUrl(); } return $this->_returnUrl; diff --git a/extensions/yii/authclient/OAuth1.php b/extensions/yii/authclient/OAuth1.php index e68118d..3eaf82f 100644 --- a/extensions/yii/authclient/OAuth1.php +++ b/extensions/yii/authclient/OAuth1.php @@ -40,15 +40,15 @@ class OAuth1 extends BaseOAuth /** * @var string OAuth consumer key. */ - public $consumerKey = ''; + public $consumerKey; /** * @var string OAuth consumer secret. */ - public $consumerSecret = ''; + public $consumerSecret; /** * @var string OAuth request token URL. */ - public $requestTokenUrl = ''; + public $requestTokenUrl; /** * @var string request token HTTP method. */ @@ -56,7 +56,7 @@ class OAuth1 extends BaseOAuth /** * @var string OAuth access token URL. */ - public $accessTokenUrl = ''; + public $accessTokenUrl; /** * @var string access token HTTP method. */ @@ -179,7 +179,7 @@ class OAuth1 extends BaseOAuth $curlOptions[CURLOPT_POSTFIELDS] = $params; } $authorizationHeader = $this->composeAuthorizationHeader($params); - if (!empty($authorizationHeader)/* && $this->curlAuthHeader*/) { + if (!empty($authorizationHeader)) { $curlOptions[CURLOPT_HTTPHEADER] = ['Content-Type: application/atom+xml', $authorizationHeader]; } break; diff --git a/extensions/yii/authclient/OAuth2.php b/extensions/yii/authclient/OAuth2.php index b6e4368..09146b8 100644 --- a/extensions/yii/authclient/OAuth2.php +++ b/extensions/yii/authclient/OAuth2.php @@ -40,15 +40,15 @@ class OAuth2 extends BaseOAuth /** * @var string OAuth client ID. */ - public $clientId = ''; + public $clientId; /** * @var string OAuth client secret. */ - public $clientSecret = ''; + public $clientSecret; /** * @var string token request URL endpoint. */ - public $tokenUrl = ''; + public $tokenUrl; /** * Composes user authorization URL. diff --git a/extensions/yii/authclient/clients/GoogleOAuth.php b/extensions/yii/authclient/clients/GoogleOAuth.php index fa56b7f..0d9ab8a 100644 --- a/extensions/yii/authclient/clients/GoogleOAuth.php +++ b/extensions/yii/authclient/clients/GoogleOAuth.php @@ -24,23 +24,27 @@ class GoogleOAuth extends OAuth2 /** * @inheritdoc */ - public function __construct($config = []) + public $authUrl = 'https://accounts.google.com/o/oauth2/auth'; + /** + * @inheritdoc + */ + public $tokenUrl = 'https://accounts.google.com/o/oauth2/token'; + /** + * @inheritdoc + */ + public $apiBaseUrl = 'https://www.googleapis.com/oauth2/v1'; + + /** + * @inheritdoc + */ + public function init() { - $config = array_merge( - [ - 'clientId' => 'anonymous', - 'clientSecret' => 'anonymous', - 'authUrl' => 'https://accounts.google.com/o/oauth2/auth', - 'tokenUrl' => 'https://accounts.google.com/o/oauth2/token', - 'apiBaseUrl' => 'https://www.googleapis.com/oauth2/v1', - 'scope' => implode(' ', [ - 'https://www.googleapis.com/auth/userinfo.profile', - 'https://www.googleapis.com/auth/userinfo.email', - ]), - ], - $config - ); - parent::__construct($config); + if ($this->scope === null) { + $this->scope = implode(' ', [ + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/userinfo.email', + ]); + } } /** diff --git a/extensions/yii/authclient/clients/Twitter.php b/extensions/yii/authclient/clients/Twitter.php index 4add039..839cfce 100644 --- a/extensions/yii/authclient/clients/Twitter.php +++ b/extensions/yii/authclient/clients/Twitter.php @@ -24,24 +24,27 @@ class Twitter extends OAuth1 /** * @inheritdoc */ - public function __construct($config = []) - { - $config = array_merge( - [ - 'consumerKey' => 'anonymous', - 'consumerSecret' => 'anonymous', - 'requestTokenUrl' => 'https://api.twitter.com/oauth/request_token', - 'requestTokenMethod' => 'POST', - 'accessTokenUrl' => 'https://api.twitter.com/oauth/access_token', - 'accessTokenMethod' => 'POST', - 'authUrl' => 'https://api.twitter.com/oauth/authorize', - 'scope' => '', - 'apiBaseUrl' => 'https://api.twitter.com/1.1', - ], - $config - ); - parent::__construct($config); - } + public $authUrl = 'https://api.twitter.com/oauth/authorize'; + /** + * @inheritdoc + */ + public $requestTokenUrl = 'https://api.twitter.com/oauth/request_token'; + /** + * @inheritdoc + */ + public $requestTokenMethod = 'POST'; + /** + * @inheritdoc + */ + public $accessTokenUrl = 'https://api.twitter.com/oauth/access_token'; + /** + * @inheritdoc + */ + public $accessTokenMethod = 'POST'; + /** + * @inheritdoc + */ + public $apiBaseUrl = 'https://api.twitter.com/1.1'; /** * @inheritdoc From b2f3bed673012c77f1538afb3ff395776b144d80 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Mon, 23 Dec 2013 17:22:40 +0200 Subject: [PATCH 23/37] OpenId client refactor in progress. --- extensions/yii/authclient/AuthAction.php | 2 +- extensions/yii/authclient/OpenId.php | 282 ++++++++++++++++++------------- 2 files changed, 167 insertions(+), 117 deletions(-) diff --git a/extensions/yii/authclient/AuthAction.php b/extensions/yii/authclient/AuthAction.php index a7e14cf..1e9b64d 100644 --- a/extensions/yii/authclient/AuthAction.php +++ b/extensions/yii/authclient/AuthAction.php @@ -233,7 +233,7 @@ class AuthAction extends Action $provider->realm = $request->getHostInfo(); $provider->returnUrl = $provider->realm . $request->getUrl(); // getting return URL - $url = $provider->authUrl(); + $url = $provider->buildAuthUrl(); return Yii::$app->getResponse()->redirect($url); } return $this->redirectCancel(); diff --git a/extensions/yii/authclient/OpenId.php b/extensions/yii/authclient/OpenId.php index 0665ceb..73c3b79 100644 --- a/extensions/yii/authclient/OpenId.php +++ b/extensions/yii/authclient/OpenId.php @@ -27,12 +27,27 @@ use yii\base\NotSupportedException; class OpenId extends BaseClient implements ClientInterface { /** - * @var array list of attributes, which should be requested from server. + * @var array list of attributes, which always should be returned from server. */ public $requiredAttributes = []; - public $optional = []; - public $verify_peer; + /** + * @var array list of attributes, which could be returned from server. + */ + public $optionalAttributes = []; + + /** + * @var boolean whether to verify the peer's certificate. + */ + public $verifyPeer; + /** + * @var string directory that holds multiple CA certificates. + * This value will take effect only if [[verifyPeer]] is set. + */ public $capath; + /** + * @var string the name of a file holding one or more certificates to verify the peer with. + * This value will take effect only if [[verifyPeer]] is set. + */ public $cainfo; private $_returnUrl; @@ -41,24 +56,33 @@ class OpenId extends BaseClient implements ClientInterface private $_trustRoot; protected $server; + /** + * @var string protocol version. + */ protected $version; protected $aliases; - protected $identifier_select = false; + /** + * @var boolean whether to request OP to select identity for an user in OpenID 2. Does not affect OpenID 1. + */ + protected $identifierSelect = 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', + /** + * @var array map of matches between AX and SREG attribute names in format: axAttributeName => sregAttributeName + */ + public $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', + 'contact/country/home' => 'country', + 'pref/language' => 'language', + 'pref/timezone' => 'timezone', ]; /** @@ -66,7 +90,7 @@ class OpenId extends BaseClient implements ClientInterface */ public function init() { - $this->data = $_POST + $_GET; # OPs may send data as POST or GET. + $this->data = $_POST + $_GET; // OPs may send data as POST or GET. } public function setIdentity($value) @@ -85,6 +109,14 @@ class OpenId extends BaseClient implements ClientInterface $this->claimed_id = $value; } + 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 setReturnUrl($returnUrl) { $this->_returnUrl = $returnUrl; @@ -99,14 +131,6 @@ class OpenId extends BaseClient implements ClientInterface 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); @@ -154,6 +178,14 @@ class OpenId extends BaseClient implements ClientInterface return !empty($ips); } + /** + * Sends HTTP request. + * @param string $url request URL. + * @param string $method request method. + * @param array $params request params. + * @return array|string response. + * @throws \yii\base\Exception on failure. + */ protected function sendCurlRequest($url, $method = 'GET', $params = []) { $params = http_build_query($params, '', '&'); @@ -164,8 +196,8 @@ class OpenId extends BaseClient implements ClientInterface 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->verifyPeer !== null) { + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $this->verifyPeer); if($this->capath) { curl_setopt($curl, CURLOPT_CAPATH, $this->capath); } @@ -209,6 +241,15 @@ class OpenId extends BaseClient implements ClientInterface return $response; } + /** + * Sends HTTP request. + * @param string $url request URL. + * @param string $method request method. + * @param array $params request params. + * @return array|string response. + * @throws \yii\base\Exception on failure. + * @throws \yii\base\NotSupportedException if request method is not supported. + */ protected function sendStreamRequest($url, $method = 'GET', $params = []) { if (!$this->hostExists($url)) { @@ -238,9 +279,9 @@ class OpenId extends BaseClient implements ClientInterface ]; 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. + /* 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' => [ @@ -256,18 +297,18 @@ class OpenId extends BaseClient implements ClientInterface return []; } - # Parsing headers. + // 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. + /* 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]; @@ -281,14 +322,14 @@ class OpenId extends BaseClient implements ClientInterface } } - # And restore them. + // and restore them stream_context_get_default($default); return $headers; default: throw new NotSupportedException("Method {$method} not supported"); } - if ($this->verify_peer) { + if ($this->verifyPeer) { $options = array_merge( $options, [ @@ -305,6 +346,13 @@ class OpenId extends BaseClient implements ClientInterface return file_get_contents($url, false, $context); } + /** + * Sends request to the server + * @param string $url request URL. + * @param string $method request method. + * @param array $params request parameters. + * @return array|string response. + */ protected function sendRequest($url, $method = 'GET', $params = []) { if (function_exists('curl_init') && !ini_get('safe_mode')) { @@ -340,7 +388,6 @@ class OpenId extends BaseClient implements ClientInterface { 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]; } @@ -349,27 +396,27 @@ class OpenId extends BaseClient implements ClientInterface * Performs Yadis and HTML discovery. Normally not used. * @param string $url Identity URL. * @return string OP Endpoint (i.e. OpenID provider address). - * @throws Exception + * @throws Exception on failure. */ public function discover($url) { if (!$url) { throw new Exception('No identity supplied.'); } - # Use xri.net proxy to resolve i-name identities + // 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. + /* 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. + // 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. + // 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'); @@ -384,22 +431,22 @@ class OpenId extends BaseClient implements ClientInterface && (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. + /* 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. + $content = ' ' . $content; // The space is added, so that strpos doesn't return 0. - # OpenID 2 + // 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; + $this->identifierSelect = true; } preg_match('#(.*)#', $content, $server); @@ -407,7 +454,7 @@ class OpenId extends BaseClient implements ClientInterface if (empty($server)) { return false; } - # Does the server advertise support for either AX or SREG? + // 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'); @@ -422,16 +469,15 @@ class OpenId extends BaseClient implements ClientInterface return $server; } - # OpenID 1.1 + // 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 + // 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'); @@ -456,7 +502,7 @@ class OpenId extends BaseClient implements ClientInterface continue; } - # There are no relevant information in headers, so we search the body. + // 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) { @@ -469,23 +515,22 @@ class OpenId extends BaseClient implements ClientInterface $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. + // 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 + // 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 + // We found an OpenID2 OP Endpoint if ($delegate) { - # We have also found an OP-Local ID. + // We have also found an OP-Local ID. $this->identity = $delegate; } $this->server = $server; @@ -499,28 +544,28 @@ class OpenId extends BaseClient implements ClientInterface 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 + /* 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->requiredAttributes) { $params['openid.sreg.required'] = []; foreach ($this->requiredAttributes as $required) { - if (!isset(self::$axToSregMap[$required])) { + if (!isset($this->axToSregMap[$required])) { continue; } - $params['openid.sreg.required'][] = self::$axToSregMap[$required]; + $params['openid.sreg.required'][] = $this->axToSregMap[$required]; } $params['openid.sreg.required'] = implode(',', $params['openid.sreg.required']); } - if ($this->optional) { + if ($this->optionalAttributes) { $params['openid.sreg.optional'] = []; - foreach ($this->optional as $optional) { - if (!isset(self::$axToSregMap[$optional])) { + foreach ($this->optionalAttributes as $optional) { + if (!isset($this->axToSregMap[$optional])) { continue; } - $params['openid.sreg.optional'][] = self::$axToSregMap[$optional]; + $params['openid.sreg.optional'][] = $this->axToSregMap[$optional]; } $params['openid.sreg.optional'] = implode(',', $params['openid.sreg.optional']); } @@ -530,14 +575,14 @@ class OpenId extends BaseClient implements ClientInterface protected function axParams() { $params = []; - if ($this->requiredAttributes || $this->optional) { + if ($this->requiredAttributes || $this->optionalAttributes) { $params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0'; $params['openid.ax.mode'] = 'fetch_request'; $this->aliases = []; $counts = []; $required = []; $optional = []; - foreach (['requiredAttributes', 'optional'] as $type) { + foreach (['requiredAttributes', 'optionalAttributes'] as $type) { foreach ($this->$type as $alias => $field) { if (is_int($alias)) { $alias = strtr($field, '/', '_'); @@ -560,8 +605,8 @@ class OpenId extends BaseClient implements ClientInterface $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. + // 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); } @@ -572,12 +617,16 @@ class OpenId extends BaseClient implements ClientInterface return $params; } - protected function authUrlV1() + /** + * Builds authentication URL for the protocol version 1. + * @return string authentication URL. + */ + protected function buildAuthUrlV1() { $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 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; } @@ -595,7 +644,12 @@ class OpenId extends BaseClient implements ClientInterface return $this->buildUrl(parse_url($this->server), ['query' => http_build_query($params, '', '&')]); } - protected function authUrlV2($identifierSelect) + /** + * Builds authentication URL for the protocol version 2. + * @param boolean $identifierSelect whether to request OP to select identity for an user. + * @return string authentication URL. + */ + protected function buildAuthUrlV2($identifierSelect) { $params = [ 'openid.ns' => 'http://specs.openid.net/auth/2.0', @@ -610,8 +664,7 @@ class OpenId extends BaseClient implements ClientInterface $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. + // 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); } @@ -628,22 +681,22 @@ class OpenId extends BaseClient implements ClientInterface /** * 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. + * @param boolean $identifierSelect 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 + * @throws Exception on failure. */ - public function authUrl($identifier_select = null) + public function buildAuthUrl($identifierSelect = null) { if (!$this->server) { $this->discover($this->identity); } if ($this->version == 2) { - if ($identifier_select === null) { - return $this->authUrlV2($this->identifier_select); + if ($identifierSelect === null) { + $identifierSelect = $this->identifierSelect; } - return $this->authUrlV2($identifier_select); + return $this->buildAuthUrlV2($identifierSelect); } - return $this->authUrlV1(); + return $this->buildAuthUrlV1(); } /** @@ -661,34 +714,33 @@ class OpenId extends BaseClient implements ClientInterface ]; 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 + /* 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 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. + // 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. + /* 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; } @@ -704,11 +756,10 @@ class OpenId extends BaseClient implements ClientInterface { $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 + // 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 + // '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_')); @@ -717,8 +768,7 @@ class OpenId extends BaseClient implements ClientInterface } } if (!$alias) { - # An alias for AX schema has not been found, - # so there is no AX data in the OP's response + // An alias for AX schema has not been found, so there is no AX data in the OP's response return []; } @@ -730,9 +780,9 @@ class OpenId extends BaseClient implements ClientInterface } $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. + /* 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/')); @@ -744,7 +794,7 @@ class OpenId extends BaseClient implements ClientInterface protected function getSregAttributes() { $attributes = []; - $sregToAx = array_flip(self::$axToSregMap); + $sregToAx = array_flip($this->axToSregMap); foreach ($this->data as $key => $value) { $keyMatch = 'openid_sreg_'; if (substr($key, 0, strlen($keyMatch)) != $keyMatch) { @@ -752,7 +802,7 @@ class OpenId extends BaseClient implements ClientInterface } $key = substr($key, strlen($keyMatch)); if (!isset($sregToAx[$key])) { - # The field name isn't part of the SREG spec, so we ignore it. + // The field name isn't part of the SREG spec, so we ignore it. continue; } $attributes[$sregToAx[$key]] = $value; @@ -761,7 +811,7 @@ class OpenId extends BaseClient implements ClientInterface } /** - * Gets AX/SREG attributes provided by OP. should be used only after successful validaton. + * Gets AX/SREG attributes provided by OP. should be used only after successful validation. * 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. @@ -772,8 +822,8 @@ class OpenId extends BaseClient implements ClientInterface 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. + // 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(); From 5a137e6deca156da8d170a281a3dd44d82c4d612 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Tue, 24 Dec 2013 14:18:49 +0200 Subject: [PATCH 24/37] OpenId client refactor in progress. --- extensions/yii/authclient/AuthAction.php | 6 +- extensions/yii/authclient/OpenId.php | 144 +++++++++++++++--------- tests/unit/extensions/authclient/OpenIdTest.php | 47 ++++++++ 3 files changed, 139 insertions(+), 58 deletions(-) create mode 100644 tests/unit/extensions/authclient/OpenIdTest.php diff --git a/extensions/yii/authclient/AuthAction.php b/extensions/yii/authclient/AuthAction.php index 1e9b64d..e283f29 100644 --- a/extensions/yii/authclient/AuthAction.php +++ b/extensions/yii/authclient/AuthAction.php @@ -206,7 +206,7 @@ class AuthAction extends Action $attributes = array( 'id' => $provider->identity ); - $rawAttributes = $provider->getAttributes(); + $rawAttributes = $provider->fetchAttributes(); foreach ($provider->requiredAttributes as $openIdAttributeName) { if (isset($rawAttributes[$openIdAttributeName])) { $attributes[$openIdAttributeName] = $rawAttributes[$openIdAttributeName]; @@ -229,10 +229,6 @@ class AuthAction extends Action } } else { //$provider->identity = $provider->authUrl; // Setting identifier - $request = Yii::$app->getRequest(); - $provider->realm = $request->getHostInfo(); - $provider->returnUrl = $provider->realm . $request->getUrl(); // getting return URL - $url = $provider->buildAuthUrl(); return Yii::$app->getResponse()->redirect($url); } diff --git a/extensions/yii/authclient/OpenId.php b/extensions/yii/authclient/OpenId.php index 73c3b79..0aa8b84 100644 --- a/extensions/yii/authclient/OpenId.php +++ b/extensions/yii/authclient/OpenId.php @@ -9,16 +9,16 @@ namespace yii\authclient; use yii\base\Exception; use yii\base\NotSupportedException; +use Yii; /** * Class Client * * @see http://openid.net/ * - * @property string $returnUrl ??? + * @property string $returnUrl authentication return URL. * @property mixed $identity ??? - * @property string $trustRoot ??? - * @property string $realm alias of [[trustRoot]]. + * @property string $trustRoot client trust root (realm), by default [[\yii\web\Request::hostInfo]] value will be used. * @property mixed $mode ??? This property is read-only. * * @author Paul Klimov @@ -50,25 +50,33 @@ class OpenId extends BaseClient implements ClientInterface */ public $cainfo; + /** + * @var string authentication return URL. + */ private $_returnUrl; private $_identity; private $claimed_id; + /** + * @var string client trust root (realm), by default [[\yii\web\Request::hostInfo]] value will be used. + */ private $_trustRoot; - protected $server; /** * @var string protocol version. */ protected $version; - protected $aliases; /** * @var boolean whether to request OP to select identity for an user in OpenID 2. Does not affect OpenID 1. */ protected $identifierSelect = false; protected $ax = false; protected $sreg = false; - protected $data; + /** + * @var array data, which should be used to retrieve the OpenID response. + * If not set combination of GET and POST will be used. + */ + public $data; /** * @var array map of matches between AX and SREG attribute names in format: axAttributeName => sregAttributeName @@ -90,7 +98,9 @@ class OpenId extends BaseClient implements ClientInterface */ public function init() { - $this->data = $_POST + $_GET; // OPs may send data as POST or GET. + if ($this->data === null) { + $this->data = array_merge($_GET, $_POST); // OPs may send data as POST or GET. + } } public function setIdentity($value) @@ -117,46 +127,63 @@ class OpenId extends BaseClient implements ClientInterface return $this->claimed_id; } + /** + * @param string $returnUrl authentication return URL. + */ public function setReturnUrl($returnUrl) { $this->_returnUrl = $returnUrl; } + /** + * @return string authentication return URL. + */ public function getReturnUrl() { if ($this->_returnUrl === null) { - $uri = rtrim(preg_replace('#((?<=\?)|&)openid\.[^&]+#', '', $_SERVER['REQUEST_URI']), '?'); - $this->_returnUrl = $this->getTrustRoot() . $uri; + $this->_returnUrl = $this->defaultReturnUrl(); } return $this->_returnUrl; } + /** + * @param string $value client trust root (realm). + */ public function setTrustRoot($value) { - $this->_trustRoot = trim($value); + $this->_trustRoot = $value; } + /** + * @return string client trust root (realm). + */ public function getTrustRoot() { if ($this->_trustRoot === null) { - $this->_trustRoot = (!empty($_SERVER['HTTPS']) ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST']; + $this->_trustRoot = Yii::$app->getRequest()->getHostInfo(); } return $this->_trustRoot; } - public function setRealm($value) - { - $this->setTrustRoot($value); - } - - public function getRealm() + public function getMode() { - return $this->getTrustRoot(); + return empty($this->data['openid_mode']) ? null : $this->data['openid_mode']; } - public function getMode() + /** + * Generates default [[returnUrl]] value. + * @return string default authentication return URL. + */ + protected function defaultReturnUrl() { - return empty($this->data['openid_mode']) ? null : $this->data['openid_mode']; + $params = $_GET; + foreach ($params as $name => $value) { + if (strncmp('openid', $name, 6) === 0) { + unset($params[$name]); + } + } + $url = Yii::$app->getUrlManager()->createUrl(Yii::$app->requestedRoute, $params); + return $this->getTrustRoot() . $url; } /** @@ -405,7 +432,7 @@ class OpenId extends BaseClient implements ClientInterface } // Use xri.net proxy to resolve i-name identities if (!preg_match('#^https?:#', $url)) { - $url = "https://xri.net/$url"; + $url = 'https://xri.net/' . $url; } /* We save the original url in case of Yadis discovery failure. @@ -455,7 +482,7 @@ class OpenId extends BaseClient implements ClientInterface 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->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'); @@ -465,7 +492,6 @@ class OpenId extends BaseClient implements ClientInterface } $this->version = 2; - $this->server = $server; return $server; } @@ -487,7 +513,6 @@ class OpenId extends BaseClient implements ClientInterface } $this->version = 1; - $this->server = $server; return $server; } } @@ -522,7 +547,7 @@ class OpenId extends BaseClient implements ClientInterface if (!$server) { // The same with openid 1.1 - $server = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid.server', 'href'); + $server = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid.server', 'href'); $delegate = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid.delegate', 'href'); $this->version = 1; } @@ -533,7 +558,6 @@ class OpenId extends BaseClient implements ClientInterface // We have also found an OP-Local ID. $this->identity = $delegate; } - $this->server = $server; return $server; } throw new Exception('No servers found!'); @@ -541,7 +565,11 @@ class OpenId extends BaseClient implements ClientInterface throw new Exception('Endless redirection!'); } - protected function sregParams() + /** + * Composes SREG request parameters. + * @return array SREG parameters. + */ + protected function buildSregParams() { $params = []; /* We always use SREG 1.1, even if the server is advertising only support for 1.0. @@ -572,13 +600,17 @@ class OpenId extends BaseClient implements ClientInterface return $params; } - protected function axParams() + /** + * Composes AX request parameters. + * @return array AX parameters. + */ + protected function buildAxParams() { $params = []; if ($this->requiredAttributes || $this->optionalAttributes) { $params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0'; $params['openid.ax.mode'] = 'fetch_request'; - $this->aliases = []; + $aliases = []; $counts = []; $required = []; $optional = []; @@ -587,7 +619,7 @@ class OpenId extends BaseClient implements ClientInterface if (is_int($alias)) { $alias = strtr($field, '/', '_'); } - $this->aliases[$alias] = 'http://axschema.org/' . $field; + $aliases[$alias] = 'http://axschema.org/' . $field; if (empty($counts[$alias])) { $counts[$alias] = 0; } @@ -595,7 +627,7 @@ class OpenId extends BaseClient implements ClientInterface ${$type}[] = $alias; } } - foreach ($this->aliases as $alias => $ns) { + foreach ($aliases as $alias => $ns) { $params['openid.ax.type.' . $alias] = $ns; } foreach ($counts as $alias => $count) { @@ -619,9 +651,10 @@ class OpenId extends BaseClient implements ClientInterface /** * Builds authentication URL for the protocol version 1. + * @param string $providerUrl OP Endpoint (i.e. OpenID provider address) * @return string authentication URL. */ - protected function buildAuthUrlV1() + protected function buildAuthUrlV1($providerUrl) { $returnUrl = $this->returnUrl; /* If we have an openid.delegate that is different from our claimed id, @@ -632,7 +665,7 @@ class OpenId extends BaseClient implements ClientInterface } $params = array_merge( - $this->sregParams(), + $this->buildSregParams(), [ 'openid.return_to' => $returnUrl, 'openid.mode' => 'checkid_setup', @@ -641,15 +674,16 @@ class OpenId extends BaseClient implements ClientInterface ] ); - return $this->buildUrl(parse_url($this->server), ['query' => http_build_query($params, '', '&')]); + return $this->buildUrl(parse_url($providerUrl), ['query' => http_build_query($params, '', '&')]); } /** * Builds authentication URL for the protocol version 2. + * @param string $providerUrl OP Endpoint (i.e. OpenID provider address) * @param boolean $identifierSelect whether to request OP to select identity for an user. * @return string authentication URL. */ - protected function buildAuthUrlV2($identifierSelect) + protected function buildAuthUrlV2($providerUrl, $identifierSelect) { $params = [ 'openid.ns' => 'http://specs.openid.net/auth/2.0', @@ -658,14 +692,14 @@ class OpenId extends BaseClient implements ClientInterface 'openid.realm' => $this->trustRoot, ]; if ($this->ax) { - $params = array_merge($this->axParams(), $params); + $params = array_merge($this->buildAxParams(), $params); } if ($this->sreg) { - $params = array_merge($this->sregParams(), $params); + $params = array_merge($this->buildSregParams(), $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); + $params = array_merge($this->buildSregParams(), $this->buildAxParams(), $params); } if ($identifierSelect) { @@ -676,7 +710,7 @@ class OpenId extends BaseClient implements ClientInterface $params['openid.identity'] = $this->identity; $params['openid.claimed_id'] = $this->claimed_id; } - return $this->buildUrl(parse_url($this->server), ['query' => http_build_query($params, '', '&')]); + return $this->buildUrl(parse_url($providerUrl), ['query' => http_build_query($params, '', '&')]); } /** @@ -687,16 +721,14 @@ class OpenId extends BaseClient implements ClientInterface */ public function buildAuthUrl($identifierSelect = null) { - if (!$this->server) { - $this->discover($this->identity); - } + $providerUrl = $this->discover($this->identity); if ($this->version == 2) { if ($identifierSelect === null) { $identifierSelect = $this->identifierSelect; } - return $this->buildAuthUrlV2($identifierSelect); + return $this->buildAuthUrlV2($providerUrl, $identifierSelect); } - return $this->buildAuthUrlV1(); + return $this->buildAuthUrlV1($providerUrl); } /** @@ -718,9 +750,7 @@ class OpenId extends BaseClient implements ClientInterface 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'] - ) { + } 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; @@ -752,7 +782,11 @@ class OpenId extends BaseClient implements ClientInterface return preg_match('/is_valid\s*:\s*true/i', $response); } - protected function getAxAttributes() + /** + * Gets AX attributes provided by OP. + * @return array array of attributes. + */ + protected function fetchAxAttributes() { $alias = null; if (isset($this->data['openid_ns_ax']) && $this->data['openid_ns_ax'] != 'http://openid.net/srv/ax/1.0') { @@ -791,7 +825,11 @@ class OpenId extends BaseClient implements ClientInterface return $attributes; } - protected function getSregAttributes() + /** + * Gets SREG attributes provided by OP. SREG names will be mapped to AX names. + * @return array array of attributes with keys being the AX schema names, e.g. 'contact/email' + */ + protected function fetchSregAttributes() { $attributes = []; $sregToAx = array_flip($this->axToSregMap); @@ -811,7 +849,7 @@ class OpenId extends BaseClient implements ClientInterface } /** - * Gets AX/SREG attributes provided by OP. should be used only after successful validation. + * Gets AX/SREG attributes provided by OP. Should be used only after successful validation. * 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. @@ -819,13 +857,13 @@ class OpenId extends BaseClient implements ClientInterface * @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() + public function fetchAttributes() { 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 array_merge($this->fetchSregAttributes(), $this->fetchAxAttributes()); } - return $this->getSregAttributes(); + return $this->fetchSregAttributes(); } } \ No newline at end of file diff --git a/tests/unit/extensions/authclient/OpenIdTest.php b/tests/unit/extensions/authclient/OpenIdTest.php new file mode 100644 index 0000000..928c3f5 --- /dev/null +++ b/tests/unit/extensions/authclient/OpenIdTest.php @@ -0,0 +1,47 @@ + [ + 'request' => [ + 'hostInfo' => 'http://testdomain.com', + 'scriptUrl' => '/index.php', + ], + ] + ]; + $this->mockApplication($config, '\yii\web\Application'); + } + + // Tests : + + public function testSetGet() + { + $client = new OpenId(); + + $trustRoot = 'http://trust.root'; + $client->setTrustRoot($trustRoot); + $this->assertEquals($trustRoot, $client->getTrustRoot(), 'Unable to setup trust root!'); + + $returnUrl = 'http://return.url'; + $client->setReturnUrl($returnUrl); + $this->assertEquals($returnUrl, $client->getReturnUrl(), 'Unable to setup return URL!'); + } + + /** + * @depends testSetGet + */ + public function testGetDefaults() + { + $client = new OpenId(); + + $this->assertNotEmpty($client->getTrustRoot(), 'Unable to get default trust root!'); + $this->assertNotEmpty($client->getReturnUrl(), 'Unable to get default return URL!'); + } +} \ No newline at end of file From 46746cd8a147eb41b6b891e98899a66776e4c8cc Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Tue, 24 Dec 2013 16:06:21 +0200 Subject: [PATCH 25/37] OpenId client 'discover()' method refactored. --- extensions/yii/authclient/OpenId.php | 117 ++++++++++++------------ tests/unit/extensions/authclient/OpenIdTest.php | 13 +++ 2 files changed, 72 insertions(+), 58 deletions(-) diff --git a/extensions/yii/authclient/OpenId.php b/extensions/yii/authclient/OpenId.php index 0aa8b84..a408ee9 100644 --- a/extensions/yii/authclient/OpenId.php +++ b/extensions/yii/authclient/OpenId.php @@ -60,24 +60,11 @@ class OpenId extends BaseClient implements ClientInterface * @var string client trust root (realm), by default [[\yii\web\Request::hostInfo]] value will be used. */ private $_trustRoot; - - /** - * @var string protocol version. - */ - protected $version; - - /** - * @var boolean whether to request OP to select identity for an user in OpenID 2. Does not affect OpenID 1. - */ - protected $identifierSelect = false; - protected $ax = false; - protected $sreg = false; /** * @var array data, which should be used to retrieve the OpenID response. * If not set combination of GET and POST will be used. */ public $data; - /** * @var array map of matches between AX and SREG attribute names in format: axAttributeName => sregAttributeName */ @@ -420,9 +407,14 @@ class OpenId extends BaseClient implements ClientInterface } /** - * Performs Yadis and HTML discovery. Normally not used. + * Performs Yadis and HTML discovery. * @param string $url Identity URL. - * @return string OP Endpoint (i.e. OpenID provider address). + * @return array OpenID provider info, following keys will be available: + * - 'url' - string OP Endpoint (i.e. OpenID provider address). + * - 'version' - integer OpenID protocol version used by provider. + * - 'identifierSelect' - boolean whether to request OP to select identity for an user in OpenID 2, does not affect OpenID 1. + * - 'ax' - boolean whether AX attributes should be used. + * - 'sreg' - boolean whether SREG attributes should be used. * @throws Exception on failure. */ public function discover($url) @@ -435,6 +427,14 @@ class OpenId extends BaseClient implements ClientInterface $url = 'https://xri.net/' . $url; } + $result = [ + 'url' => null, + 'version' => null, + 'identifierSelect' => false, + 'ax' => false, + 'sreg' => false, + ]; + /* 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.*/ @@ -473,26 +473,26 @@ class OpenId extends BaseClient implements ClientInterface $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->identifierSelect = true; + $result['identifierSelect'] = true; } preg_match('#(.*)#', $content, $server); preg_match('#<(Local|Canonical)ID>(.*)#', $content, $delegate); if (empty($server)) { - return false; + throw new Exception('No servers found!'); } // 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'); + $result['ax'] = (bool) strpos($content, 'http://openid.net/srv/ax/1.0'); + $result['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; - return $server; + $result['url'] = $server; + $result['version'] = 2; + return $result; } // OpenID 1.1 @@ -501,19 +501,19 @@ class OpenId extends BaseClient implements ClientInterface preg_match('#(.*)#', $content, $server); preg_match('#<.*?Delegate>(.*)#', $content, $delegate); if (empty($server)) { - return false; + throw new Exception('No servers found!'); } // 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'); + $result['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; - return $server; + $result['url'] = $server; + $result['version'] = 1; + return $result; } } @@ -542,14 +542,14 @@ class OpenId extends BaseClient implements ClientInterface // 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; + $version = 1; + } else { + $delegate = $this->extractHtmlTagValue($content, 'link', 'rel', 'openid2.local_id', 'href'); + $version = 2; } if ($server) { @@ -558,7 +558,9 @@ class OpenId extends BaseClient implements ClientInterface // We have also found an OP-Local ID. $this->identity = $delegate; } - return $server; + $result['url'] = $server; + $result['version'] = $version; + return $result; } throw new Exception('No servers found!'); } @@ -574,9 +576,9 @@ class OpenId extends BaseClient implements ClientInterface $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*/ + 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->requiredAttributes) { + if (!empty($this->requiredAttributes)) { $params['openid.sreg.required'] = []; foreach ($this->requiredAttributes as $required) { if (!isset($this->axToSregMap[$required])) { @@ -587,7 +589,7 @@ class OpenId extends BaseClient implements ClientInterface $params['openid.sreg.required'] = implode(',', $params['openid.sreg.required']); } - if ($this->optionalAttributes) { + if (!empty($this->optionalAttributes)) { $params['openid.sreg.optional'] = []; foreach ($this->optionalAttributes as $optional) { if (!isset($this->axToSregMap[$optional])) { @@ -607,7 +609,7 @@ class OpenId extends BaseClient implements ClientInterface protected function buildAxParams() { $params = []; - if ($this->requiredAttributes || $this->optionalAttributes) { + if (!empty($this->requiredAttributes) || !empty($this->optionalAttributes)) { $params['openid.ns.ax'] = 'http://openid.net/srv/ax/1.0'; $params['openid.ax.mode'] = 'fetch_request'; $aliases = []; @@ -651,12 +653,12 @@ class OpenId extends BaseClient implements ClientInterface /** * Builds authentication URL for the protocol version 1. - * @param string $providerUrl OP Endpoint (i.e. OpenID provider address) + * @param array $serverInfo OpenID server info. * @return string authentication URL. */ - protected function buildAuthUrlV1($providerUrl) + protected function buildAuthUrlV1($serverInfo) { - $returnUrl = $this->returnUrl; + $returnUrl = $this->getReturnUrl(); /* 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.*/ @@ -674,35 +676,34 @@ class OpenId extends BaseClient implements ClientInterface ] ); - return $this->buildUrl(parse_url($providerUrl), ['query' => http_build_query($params, '', '&')]); + return $this->buildUrl(parse_url($serverInfo['url']), ['query' => http_build_query($params, '', '&')]); } /** * Builds authentication URL for the protocol version 2. - * @param string $providerUrl OP Endpoint (i.e. OpenID provider address) - * @param boolean $identifierSelect whether to request OP to select identity for an user. + * @param array $serverInfo OpenID server info. * @return string authentication URL. */ - protected function buildAuthUrlV2($providerUrl, $identifierSelect) + protected function buildAuthUrlV2($serverInfo) { $params = [ 'openid.ns' => 'http://specs.openid.net/auth/2.0', 'openid.mode' => 'checkid_setup', - 'openid.return_to' => $this->returnUrl, - 'openid.realm' => $this->trustRoot, + 'openid.return_to' => $this->getReturnUrl(), + 'openid.realm' => $this->getTrustRoot(), ]; - if ($this->ax) { + if ($serverInfo['ax']) { $params = array_merge($this->buildAxParams(), $params); } - if ($this->sreg) { + if ($serverInfo['sreg']) { $params = array_merge($this->buildSregParams(), $params); } - if (!$this->ax && !$this->sreg) { + if (!$serverInfo['ax'] && !$serverInfo['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->buildSregParams(), $this->buildAxParams(), $params); } - if ($identifierSelect) { + if ($serverInfo['identifierSelect']) { $url = 'http://specs.openid.net/auth/2.0/identifier_select'; $params['openid.identity'] = $url; $params['openid.claimed_id']= $url; @@ -710,7 +711,7 @@ class OpenId extends BaseClient implements ClientInterface $params['openid.identity'] = $this->identity; $params['openid.claimed_id'] = $this->claimed_id; } - return $this->buildUrl(parse_url($providerUrl), ['query' => http_build_query($params, '', '&')]); + return $this->buildUrl(parse_url($serverInfo['url']), ['query' => http_build_query($params, '', '&')]); } /** @@ -721,14 +722,14 @@ class OpenId extends BaseClient implements ClientInterface */ public function buildAuthUrl($identifierSelect = null) { - $providerUrl = $this->discover($this->identity); - if ($this->version == 2) { - if ($identifierSelect === null) { - $identifierSelect = $this->identifierSelect; + $serverInfo = $this->discover($this->identity); + if ($serverInfo['version'] == 2) { + if ($identifierSelect !== null) { + $serverInfo['identifierSelect'] = $identifierSelect; } - return $this->buildAuthUrlV2($providerUrl, $identifierSelect); + return $this->buildAuthUrlV2($serverInfo); } - return $this->buildAuthUrlV1($providerUrl); + return $this->buildAuthUrlV1($serverInfo); } /** @@ -762,7 +763,7 @@ class OpenId extends BaseClient implements ClientInterface return false; } - $server = $this->discover($this->claimed_id); + $serverInfo = $this->discover($this->claimed_id); foreach (explode(',', $this->data['openid_signed']) as $item) { /* Checking whether magic_quotes_gpc is turned on, because @@ -777,7 +778,7 @@ class OpenId extends BaseClient implements ClientInterface $params['openid.mode'] = 'check_authentication'; - $response = $this->sendRequest($server, 'POST', $params); + $response = $this->sendRequest($serverInfo['url'], 'POST', $params); return preg_match('/is_valid\s*:\s*true/i', $response); } diff --git a/tests/unit/extensions/authclient/OpenIdTest.php b/tests/unit/extensions/authclient/OpenIdTest.php index 928c3f5..b57db91 100644 --- a/tests/unit/extensions/authclient/OpenIdTest.php +++ b/tests/unit/extensions/authclient/OpenIdTest.php @@ -44,4 +44,17 @@ class OpenIdTest extends TestCase $this->assertNotEmpty($client->getTrustRoot(), 'Unable to get default trust root!'); $this->assertNotEmpty($client->getReturnUrl(), 'Unable to get default return URL!'); } + + public function testDiscover() + { + $url = 'https://www.google.com/accounts/o8/id'; + $client = new OpenId(); + $info = $client->discover($url); + $this->assertNotEmpty($info); + $this->assertNotEmpty($info['url']); + $this->assertEquals(2, $info['version']); + $this->assertArrayHasKey('identifierSelect', $info); + $this->assertArrayHasKey('ax', $info); + $this->assertArrayHasKey('sreg', $info); + } } \ No newline at end of file From fea6520070fcd292824ea9e48171443bbaf8cc2a Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Tue, 24 Dec 2013 16:59:47 +0200 Subject: [PATCH 26/37] OpenId client identiy/claimed_id separation fixed. --- extensions/yii/authclient/OpenId.php | 68 +++++++++++++++++------------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/extensions/yii/authclient/OpenId.php b/extensions/yii/authclient/OpenId.php index a408ee9..0a3e49d 100644 --- a/extensions/yii/authclient/OpenId.php +++ b/extensions/yii/authclient/OpenId.php @@ -19,7 +19,6 @@ use Yii; * @property string $returnUrl authentication return URL. * @property mixed $identity ??? * @property string $trustRoot client trust root (realm), by default [[\yii\web\Request::hostInfo]] value will be used. - * @property mixed $mode ??? This property is read-only. * * @author Paul Klimov * @since 2.0 @@ -54,8 +53,10 @@ class OpenId extends BaseClient implements ClientInterface * @var string authentication return URL. */ private $_returnUrl; + private $_identity; - private $claimed_id; + + private $_claimedId; /** * @var string client trust root (realm), by default [[\yii\web\Request::hostInfo]] value will be used. */ @@ -103,7 +104,7 @@ class OpenId extends BaseClient implements ClientInterface } } $this->_identity = $value; - $this->claimed_id = $value; + $this->_claimedId = $value; } public function getIdentity() @@ -111,7 +112,7 @@ class OpenId extends BaseClient implements ClientInterface /* 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; + return $this->_claimedId; } /** @@ -152,11 +153,6 @@ class OpenId extends BaseClient implements ClientInterface return $this->_trustRoot; } - public function getMode() - { - return empty($this->data['openid_mode']) ? null : $this->data['openid_mode']; - } - /** * Generates default [[returnUrl]] value. * @return string default authentication return URL. @@ -234,15 +230,15 @@ class OpenId extends BaseClient implements ClientInterface if ($method == 'HEAD') { $headers = []; foreach (explode("\n", $response) as $header) { - $pos = strpos($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; + // Updating claimed_id in case of redirections. + $effectiveUrl = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL); + if ($effectiveUrl != $url) { + $this->_identity = $this->_claimedId = $effectiveUrl; } return $headers; @@ -306,17 +302,17 @@ class OpenId extends BaseClient implements ClientInterface ]); $url = $url . ($params ? '?' . $params : ''); - $headers_tmp = get_headers($url); - if (!$headers_tmp) { + $headersTmp = get_headers($url); + if (empty($headersTmp)) { return []; } // Parsing headers. $headers = []; - foreach ($headers_tmp as $header) { + foreach ($headersTmp as $header) { $pos = strpos($header, ':'); $name = strtolower(trim(substr($header, 0, $pos))); - $headers[$name] = trim(substr($header, $pos+1)); + $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 @@ -325,12 +321,12 @@ class OpenId extends BaseClient implements ClientInterface 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]; + $this->_identity = $this->_claimedId = $headers[$name]; } elseif($headers[$name][0] == '/') { - $parsed_url = parse_url($this->claimed_id); - $this->identity = - $this->claimed_id = $parsed_url['scheme'] . '://' - . $parsed_url['host'] + $parsedUrl = parse_url($this->_claimedId); + $this->_identity = + $this->_claimedId = $parsedUrl['scheme'] . '://' + . $parsedUrl['host'] . $headers[$name]; } } @@ -487,7 +483,7 @@ class OpenId extends BaseClient implements ClientInterface $server = $server[1]; if (isset($delegate[2])) { - $this->identity = trim($delegate[2]); + $this->_identity = trim($delegate[2]); } $result['url'] = $server; @@ -508,7 +504,7 @@ class OpenId extends BaseClient implements ClientInterface $server = $server[1]; if (isset($delegate[1])) { - $this->identity = $delegate[1]; + $this->_identity = $delegate[1]; } $result['url'] = $server; @@ -556,7 +552,7 @@ class OpenId extends BaseClient implements ClientInterface // We found an OpenID2 OP Endpoint if ($delegate) { // We have also found an OP-Local ID. - $this->identity = $delegate; + $this->_identity = $delegate; } $result['url'] = $server; $result['version'] = $version; @@ -662,8 +658,8 @@ class OpenId extends BaseClient implements ClientInterface /* 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; + if ($this->_identity != $this->_claimedId) { + $returnUrl .= (strpos($returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $this->_claimedId; } $params = array_merge( @@ -671,7 +667,7 @@ class OpenId extends BaseClient implements ClientInterface [ 'openid.return_to' => $returnUrl, 'openid.mode' => 'checkid_setup', - 'openid.identity' => $this->identity, + 'openid.identity' => $this->_identity, 'openid.trust_root' => $this->trustRoot, ] ); @@ -708,21 +704,21 @@ class OpenId extends BaseClient implements ClientInterface $params['openid.identity'] = $url; $params['openid.claimed_id']= $url; } else { - $params['openid.identity'] = $this->identity; - $params['openid.claimed_id'] = $this->claimed_id; + $params['openid.identity'] = $this->_identity; + $params['openid.claimed_id'] = $this->_claimedId; } return $this->buildUrl(parse_url($serverInfo['url']), ['query' => http_build_query($params, '', '&')]); } /** * Returns authentication URL. Usually, you want to redirect your user to it. - * @param boolean $identifierSelect whether to request OP to select identity for an user in OpenID 2. Does not affect OpenID 1. + * @param boolean $identifierSelect 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 on failure. */ public function buildAuthUrl($identifierSelect = null) { - $serverInfo = $this->discover($this->identity); + $serverInfo = $this->discover($this->_identity); if ($serverInfo['version'] == 2) { if ($identifierSelect !== null) { $serverInfo['identifierSelect'] = $identifierSelect; @@ -739,7 +735,7 @@ class OpenId extends BaseClient implements ClientInterface */ public function validate() { - $this->claimed_id = isset($this->data['openid_claimed_id']) ? $this->data['openid_claimed_id'] : $this->data['openid_identity']; + $this->_claimedId = 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'], @@ -754,7 +750,7 @@ class OpenId extends BaseClient implements ClientInterface } 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; + $this->returnUrl .= (strpos($this->returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $this->_claimedId; } if ($this->data['openid_return_to'] != $this->returnUrl) { @@ -763,7 +759,7 @@ class OpenId extends BaseClient implements ClientInterface return false; } - $serverInfo = $this->discover($this->claimed_id); + $serverInfo = $this->discover($this->_claimedId); foreach (explode(',', $this->data['openid_signed']) as $item) { /* Checking whether magic_quotes_gpc is turned on, because From f0b9e1503d7ac62e7cdb1aa8f10ee660b6aec4b9 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Tue, 24 Dec 2013 17:25:35 +0200 Subject: [PATCH 27/37] OpenId client 'discover()' method updated to return 'identity' instead of set internal field. --- extensions/yii/authclient/OpenId.php | 36 +++++++++++++------------ tests/unit/extensions/authclient/OpenIdTest.php | 3 ++- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/extensions/yii/authclient/OpenId.php b/extensions/yii/authclient/OpenId.php index 0a3e49d..d5a00e8 100644 --- a/extensions/yii/authclient/OpenId.php +++ b/extensions/yii/authclient/OpenId.php @@ -408,29 +408,31 @@ class OpenId extends BaseClient implements ClientInterface * @return array OpenID provider info, following keys will be available: * - 'url' - string OP Endpoint (i.e. OpenID provider address). * - 'version' - integer OpenID protocol version used by provider. - * - 'identifierSelect' - boolean whether to request OP to select identity for an user in OpenID 2, does not affect OpenID 1. + * - 'identity' - string identity value. + * - 'identifier_select' - boolean whether to request OP to select identity for an user in OpenID 2, does not affect OpenID 1. * - 'ax' - boolean whether AX attributes should be used. * - 'sreg' - boolean whether SREG attributes should be used. * @throws Exception on failure. */ public function discover($url) { - if (!$url) { + if (empty($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; - } - $result = [ 'url' => null, 'version' => null, - 'identifierSelect' => false, + 'identity' => $url, + 'identifier_select' => false, 'ax' => false, 'sreg' => false, ]; + // 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.*/ @@ -469,7 +471,7 @@ class OpenId extends BaseClient implements ClientInterface $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') { - $result['identifierSelect'] = true; + $result['identifier_select'] = true; } preg_match('#(.*)#', $content, $server); @@ -483,7 +485,7 @@ class OpenId extends BaseClient implements ClientInterface $server = $server[1]; if (isset($delegate[2])) { - $this->_identity = trim($delegate[2]); + $result['identity'] = trim($delegate[2]); } $result['url'] = $server; @@ -504,7 +506,7 @@ class OpenId extends BaseClient implements ClientInterface $server = $server[1]; if (isset($delegate[1])) { - $this->_identity = $delegate[1]; + $result['identity'] = $delegate[1]; } $result['url'] = $server; @@ -552,7 +554,7 @@ class OpenId extends BaseClient implements ClientInterface // We found an OpenID2 OP Endpoint if ($delegate) { // We have also found an OP-Local ID. - $this->_identity = $delegate; + $result['identity'] = $delegate; } $result['url'] = $server; $result['version'] = $version; @@ -658,7 +660,7 @@ class OpenId extends BaseClient implements ClientInterface /* 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->_claimedId) { + if ($serverInfo['identity'] != $this->_claimedId) { $returnUrl .= (strpos($returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $this->_claimedId; } @@ -667,7 +669,7 @@ class OpenId extends BaseClient implements ClientInterface [ 'openid.return_to' => $returnUrl, 'openid.mode' => 'checkid_setup', - 'openid.identity' => $this->_identity, + 'openid.identity' => $serverInfo['identity'], 'openid.trust_root' => $this->trustRoot, ] ); @@ -699,12 +701,12 @@ class OpenId extends BaseClient implements ClientInterface $params = array_merge($this->buildSregParams(), $this->buildAxParams(), $params); } - if ($serverInfo['identifierSelect']) { + if ($serverInfo['identifier_select']) { $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.identity'] = $serverInfo['identity']; $params['openid.claimed_id'] = $this->_claimedId; } return $this->buildUrl(parse_url($serverInfo['url']), ['query' => http_build_query($params, '', '&')]); @@ -721,7 +723,7 @@ class OpenId extends BaseClient implements ClientInterface $serverInfo = $this->discover($this->_identity); if ($serverInfo['version'] == 2) { if ($identifierSelect !== null) { - $serverInfo['identifierSelect'] = $identifierSelect; + $serverInfo['identifier_select'] = $identifierSelect; } return $this->buildAuthUrlV2($serverInfo); } diff --git a/tests/unit/extensions/authclient/OpenIdTest.php b/tests/unit/extensions/authclient/OpenIdTest.php index b57db91..57dd899 100644 --- a/tests/unit/extensions/authclient/OpenIdTest.php +++ b/tests/unit/extensions/authclient/OpenIdTest.php @@ -52,8 +52,9 @@ class OpenIdTest extends TestCase $info = $client->discover($url); $this->assertNotEmpty($info); $this->assertNotEmpty($info['url']); + $this->assertNotEmpty($info['identity']); $this->assertEquals(2, $info['version']); - $this->assertArrayHasKey('identifierSelect', $info); + $this->assertArrayHasKey('identifier_select', $info); $this->assertArrayHasKey('ax', $info); $this->assertArrayHasKey('sreg', $info); } From 68db74a947f9db2a7415c9c1fc6bb75367263f3f Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Wed, 25 Dec 2013 12:08:04 +0200 Subject: [PATCH 28/37] OpenId client "authUrl" field added, identity/claimedId processing refactored. --- extensions/yii/authclient/AuthAction.php | 23 +++-- extensions/yii/authclient/OpenId.php | 104 +++++++++------------ extensions/yii/authclient/clients/GoogleOpenId.php | 21 ++--- extensions/yii/authclient/clients/YandexOpenId.php | 17 ++-- 4 files changed, 71 insertions(+), 94 deletions(-) diff --git a/extensions/yii/authclient/AuthAction.php b/extensions/yii/authclient/AuthAction.php index e283f29..65a6c7c 100644 --- a/extensions/yii/authclient/AuthAction.php +++ b/extensions/yii/authclient/AuthAction.php @@ -192,30 +192,30 @@ class AuthAction extends Action } /** - * @param OpenId $provider provider instance. + * @param OpenId $client provider instance. * @return \yii\web\Response action response. * @throws Exception on failure * @throws \yii\web\HttpException */ - protected function authOpenId($provider) + protected function authOpenId($client) { if (!empty($_REQUEST['openid_mode'])) { switch ($_REQUEST['openid_mode']) { case 'id_res': - if ($provider->validate()) { - $attributes = array( - 'id' => $provider->identity - ); - $rawAttributes = $provider->fetchAttributes(); - foreach ($provider->requiredAttributes as $openIdAttributeName) { + if ($client->validate()) { + $attributes = [ + 'id' => $client->getClaimedId() + ]; + $rawAttributes = $client->fetchAttributes(); + foreach ($client->requiredAttributes as $openIdAttributeName) { if (isset($rawAttributes[$openIdAttributeName])) { $attributes[$openIdAttributeName] = $rawAttributes[$openIdAttributeName]; } else { throw new Exception('Unable to complete the authentication because the required data was not received.'); } } - $provider->setUserAttributes($attributes); - return $this->authSuccess($provider); + $client->setUserAttributes($attributes); + return $this->authSuccess($client); } else { throw new Exception('Unable to complete the authentication because the required data was not received.'); } @@ -228,8 +228,7 @@ class AuthAction extends Action break; } } else { - //$provider->identity = $provider->authUrl; // Setting identifier - $url = $provider->buildAuthUrl(); + $url = $client->buildAuthUrl(); return Yii::$app->getResponse()->redirect($url); } return $this->redirectCancel(); diff --git a/extensions/yii/authclient/OpenId.php b/extensions/yii/authclient/OpenId.php index d5a00e8..f3e6f8f 100644 --- a/extensions/yii/authclient/OpenId.php +++ b/extensions/yii/authclient/OpenId.php @@ -17,7 +17,7 @@ use Yii; * @see http://openid.net/ * * @property string $returnUrl authentication return URL. - * @property mixed $identity ??? + * @property string $claimedId claimed identifier (identity). * @property string $trustRoot client trust root (realm), by default [[\yii\web\Request::hostInfo]] value will be used. * * @author Paul Klimov @@ -26,6 +26,11 @@ use Yii; class OpenId extends BaseClient implements ClientInterface { /** + * @var string authentication base URL, which should be used to compose actual authentication URL + * by [[buildAuthUrl()]] method. + */ + public $authUrl; + /** * @var array list of attributes, which always should be returned from server. */ public $requiredAttributes = []; @@ -53,9 +58,9 @@ class OpenId extends BaseClient implements ClientInterface * @var string authentication return URL. */ private $_returnUrl; - - private $_identity; - + /** + * @var string claimed identifier (identity) + */ private $_claimedId; /** * @var string client trust root (realm), by default [[\yii\web\Request::hostInfo]] value will be used. @@ -91,27 +96,26 @@ class OpenId extends BaseClient implements ClientInterface } } - public function setIdentity($value) + /** + * @param string $claimedId claimed identifier (identity). + */ + public function setClaimedId($claimedId) { - 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->_claimedId = $value; + $this->_claimedId = $claimedId; } - public function getIdentity() + /** + * @return string claimed identifier (identity). + */ + public function getClaimedId() { - /* 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)*/ + if ($this->_claimedId === null) { + if (isset($this->data['openid_claimed_id'])) { + $this->_claimedId = $this->data['openid_claimed_id']; + } elseif (isset($this->data['openid_identity'])) { + $this->_claimedId = $this->data['openid_identity']; + } + } return $this->_claimedId; } @@ -234,13 +238,6 @@ class OpenId extends BaseClient implements ClientInterface $name = strtolower(trim(substr($header, 0, $pos))); $headers[$name] = trim(substr($header, $pos+1)); } - - // Updating claimed_id in case of redirections. - $effectiveUrl = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL); - if ($effectiveUrl != $url) { - $this->_identity = $this->_claimedId = $effectiveUrl; - } - return $headers; } @@ -313,23 +310,6 @@ class OpenId extends BaseClient implements ClientInterface $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->_claimedId = $headers[$name]; - } elseif($headers[$name][0] == '/') { - $parsedUrl = parse_url($this->_claimedId); - $this->_identity = - $this->_claimedId = $parsedUrl['scheme'] . '://' - . $parsedUrl['host'] - . $headers[$name]; - } - } } // and restore them @@ -660,8 +640,8 @@ class OpenId extends BaseClient implements ClientInterface /* 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 ($serverInfo['identity'] != $this->_claimedId) { - $returnUrl .= (strpos($returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $this->_claimedId; + if ($serverInfo['identity'] != $this->getClaimedId()) { + $returnUrl .= (strpos($returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $this->getClaimedId(); } $params = array_merge( @@ -707,7 +687,7 @@ class OpenId extends BaseClient implements ClientInterface $params['openid.claimed_id']= $url; } else { $params['openid.identity'] = $serverInfo['identity']; - $params['openid.claimed_id'] = $this->_claimedId; + $params['openid.claimed_id'] = $this->getClaimedId(); } return $this->buildUrl(parse_url($serverInfo['url']), ['query' => http_build_query($params, '', '&')]); } @@ -720,7 +700,12 @@ class OpenId extends BaseClient implements ClientInterface */ public function buildAuthUrl($identifierSelect = null) { - $serverInfo = $this->discover($this->_identity); + $authUrl = $this->authUrl; + $claimedId = $this->getClaimedId(); + if (empty($claimedId)) { + $this->setClaimedId($authUrl); + } + $serverInfo = $this->discover($authUrl); if ($serverInfo['version'] == 2) { if ($identifierSelect !== null) { $serverInfo['identifier_select'] = $identifierSelect; @@ -733,11 +718,13 @@ class OpenId extends BaseClient implements ClientInterface /** * Performs OpenID verification with the OP. * @return boolean whether the verification was successful. - * @throws Exception */ public function validate() { - $this->_claimedId = isset($this->data['openid_claimed_id']) ? $this->data['openid_claimed_id'] : $this->data['openid_identity']; + $claimedId = $this->getClaimedId(); + if (empty($claimedId)) { + return false; + } $params = [ 'openid.assoc_handle' => $this->data['openid_assoc_handle'], 'openid.signed' => $this->data['openid_signed'], @@ -751,27 +738,20 @@ class OpenId extends BaseClient implements ClientInterface $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->_claimedId; + // we have to append it to the returnUrl, like authUrlV1 does. + $this->returnUrl .= (strpos($this->returnUrl, '?') ? '&' : '?') . 'openid.claimed_id=' . $claimedId; } 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; } - $serverInfo = $this->discover($this->_claimedId); + $serverInfo = $this->discover($claimedId); 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.' . $item] = $value; } $params['openid.mode'] = 'check_authentication'; diff --git a/extensions/yii/authclient/clients/GoogleOpenId.php b/extensions/yii/authclient/clients/GoogleOpenId.php index aa67327..1f8ebb7 100644 --- a/extensions/yii/authclient/clients/GoogleOpenId.php +++ b/extensions/yii/authclient/clients/GoogleOpenId.php @@ -20,17 +20,16 @@ class GoogleOpenId extends OpenId /** * @inheritdoc */ - public function init() - { - parent::init(); - $this->setIdentity('https://www.google.com/accounts/o8/id'); - $this->requiredAttributes = [ - 'namePerson/first', - 'namePerson/last', - 'contact/email', - 'pref/language', - ]; - } + public $authUrl = 'https://www.google.com/accounts/o8/id'; + /** + * @inheritdoc + */ + public $requiredAttributes = [ + 'namePerson/first', + 'namePerson/last', + 'contact/email', + 'pref/language', + ]; /** * @inheritdoc diff --git a/extensions/yii/authclient/clients/YandexOpenId.php b/extensions/yii/authclient/clients/YandexOpenId.php index 24c5338..0c848ba 100644 --- a/extensions/yii/authclient/clients/YandexOpenId.php +++ b/extensions/yii/authclient/clients/YandexOpenId.php @@ -20,15 +20,14 @@ class YandexOpenId extends OpenId /** * @inheritdoc */ - public function init() - { - parent::init(); - $this->setIdentity('http://openid.yandex.ru'); - $this->requiredAttributes = [ - 'namePerson', - 'contact/email', - ]; - } + public $authUrl = 'http://openid.yandex.ru'; + /** + * @inheritdoc + */ + public $requiredAttributes = [ + 'namePerson', + 'contact/email', + ]; /** * @inheritdoc From d21c59bc3ceb21b3a61b3c373b9caf7587901a03 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Wed, 25 Dec 2013 12:42:29 +0200 Subject: [PATCH 29/37] OpenId client "buildUrl()" method refactored. --- extensions/yii/authclient/OpenId.php | 49 ++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/extensions/yii/authclient/OpenId.php b/extensions/yii/authclient/OpenId.php index f3e6f8f..8751e76 100644 --- a/extensions/yii/authclient/OpenId.php +++ b/extensions/yii/authclient/OpenId.php @@ -351,22 +351,33 @@ class OpenId extends BaseClient implements ClientInterface return $this->sendStreamRequest($url, $method, $params); } - protected function buildUrl($url, $parts) + /** + * Combines given URLs into single one. + * @param string $baseUrl base URL. + * @param string|array $additionalUrl additional URL string or information array. + * @return string composed URL. + */ + protected function buildUrl($baseUrl, $additionalUrl) { - 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']}"); + $baseUrl = parse_url($baseUrl); + if (!is_array($additionalUrl)) { + $additionalUrl = parse_url($additionalUrl); + } + + if (isset($baseUrl['query'], $additionalUrl['query'])) { + $additionalUrl['query'] = $baseUrl['query'] . '&' . $additionalUrl['query']; + } + + $urlInfo = array_merge($baseUrl, $additionalUrl); + $url = $urlInfo['scheme'] . '://' + . (empty($urlInfo['username']) ? '' + :(empty($urlInfo['password']) ? "{$urlInfo['username']}@" + :"{$urlInfo['username']}:{$urlInfo['password']}@")) + . $urlInfo['host'] + . (empty($urlInfo['port']) ? '' : ":{$urlInfo['port']}") + . (empty($urlInfo['path']) ? '' : $urlInfo['path']) + . (empty($urlInfo['query']) ? '' : "?{$urlInfo['query']}") + . (empty($urlInfo['fragment']) ? '' : "#{$urlInfo['fragment']}"); return $url; } @@ -428,7 +439,7 @@ class OpenId extends BaseClient implements ClientInterface $next = false; if (isset($headers['x-xrds-location'])) { - $url = $this->buildUrl(parse_url($url), parse_url(trim($headers['x-xrds-location']))); + $url = $this->buildUrl($url, trim($headers['x-xrds-location'])); $next = true; } @@ -509,7 +520,7 @@ class OpenId extends BaseClient implements ClientInterface $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)); + $url = $this->buildUrl($url, $location); continue; } } @@ -654,7 +665,7 @@ class OpenId extends BaseClient implements ClientInterface ] ); - return $this->buildUrl(parse_url($serverInfo['url']), ['query' => http_build_query($params, '', '&')]); + return $this->buildUrl($serverInfo['url'], ['query' => http_build_query($params, '', '&')]); } /** @@ -689,7 +700,7 @@ class OpenId extends BaseClient implements ClientInterface $params['openid.identity'] = $serverInfo['identity']; $params['openid.claimed_id'] = $this->getClaimedId(); } - return $this->buildUrl(parse_url($serverInfo['url']), ['query' => http_build_query($params, '', '&')]); + return $this->buildUrl($serverInfo['url'], ['query' => http_build_query($params, '', '&')]); } /** From bb37b7b5893ff3c1b1d78b6156ee5e49f3f8f5f4 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Wed, 25 Dec 2013 13:15:54 +0200 Subject: [PATCH 30/37] OpenId attribute validation and extraction updated. --- extensions/yii/authclient/AuthAction.php | 14 +------- extensions/yii/authclient/OpenId.php | 55 +++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/extensions/yii/authclient/AuthAction.php b/extensions/yii/authclient/AuthAction.php index 65a6c7c..7584b0d 100644 --- a/extensions/yii/authclient/AuthAction.php +++ b/extensions/yii/authclient/AuthAction.php @@ -203,21 +203,9 @@ class AuthAction extends Action switch ($_REQUEST['openid_mode']) { case 'id_res': if ($client->validate()) { - $attributes = [ - 'id' => $client->getClaimedId() - ]; - $rawAttributes = $client->fetchAttributes(); - foreach ($client->requiredAttributes as $openIdAttributeName) { - if (isset($rawAttributes[$openIdAttributeName])) { - $attributes[$openIdAttributeName] = $rawAttributes[$openIdAttributeName]; - } else { - throw new Exception('Unable to complete the authentication because the required data was not received.'); - } - } - $client->setUserAttributes($attributes); return $this->authSuccess($client); } else { - throw new Exception('Unable to complete the authentication because the required data was not received.'); + throw new HttpException(400, 'Unable to complete the authentication because the required data was not received.'); } break; case 'cancel': diff --git a/extensions/yii/authclient/OpenId.php b/extensions/yii/authclient/OpenId.php index 8751e76..35527e9 100644 --- a/extensions/yii/authclient/OpenId.php +++ b/extensions/yii/authclient/OpenId.php @@ -382,13 +382,18 @@ class OpenId extends BaseClient implements ClientInterface } /** - * Helper function used to scan for / tags and extract information - * from them - */ - protected function extractHtmlTagValue($content, $tag, $attrName, $attrValue, $valueName) + * Scans content for / tags and extract information from them. + * @param string $content HTML content to be be parsed. + * @param string $tag name of the source tag. + * @param string $matchAttributeName name of the source tag attribute, which should contain $matchAttributeValue + * @param string $matchAttributeValue required value of $matchAttributeName + * @param string $valueAttributeName name of the source tag attribute, which should contain searched value. + * @return string|boolean searched value, "false" on failure. + */ + protected function extractHtmlTagValue($content, $tag, $matchAttributeName, $matchAttributeValue, $valueAttributeName) { - preg_match_all("#<{$tag}[^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*$valueName=['\"](.+?)['\"][^>]*/?>#i", $content, $matches1); - preg_match_all("#<{$tag}[^>]*$valueName=['\"](.+?)['\"][^>]*$attrName=['\"].*?$attrValue.*?['\"][^>]*/?>#i", $content, $matches2); + preg_match_all("#<{$tag}[^>]*$matchAttributeName=['\"].*?$matchAttributeValue.*?['\"][^>]*$valueAttributeName=['\"](.+?)['\"][^>]*/?>#i", $content, $matches1); + preg_match_all("#<{$tag}[^>]*$valueAttributeName=['\"](.+?)['\"][^>]*$matchAttributeName=['\"].*?$matchAttributeValue.*?['\"][^>]*/?>#i", $content, $matches2); $result = array_merge($matches1[1], $matches2[1]); return empty($result) ? false : $result[0]; } @@ -728,9 +733,10 @@ class OpenId extends BaseClient implements ClientInterface /** * Performs OpenID verification with the OP. + * @param boolean $validateRequiredAttributes whether to validate required attributes. * @return boolean whether the verification was successful. */ - public function validate() + public function validate($validateRequiredAttributes = true) { $claimedId = $this->getClaimedId(); if (empty($claimedId)) { @@ -769,7 +775,32 @@ class OpenId extends BaseClient implements ClientInterface $response = $this->sendRequest($serverInfo['url'], 'POST', $params); - return preg_match('/is_valid\s*:\s*true/i', $response); + if (preg_match('/is_valid\s*:\s*true/i', $response)) { + if ($validateRequiredAttributes) { + return $this->validateRequiredAttributes(); + } else { + return true; + } + } else { + return false; + } + } + + /** + * Checks if all required attributes are present in the server response. + * @return boolean whether all required attributes are present. + */ + protected function validateRequiredAttributes() + { + if (!empty($this->requiredAttributes)) { + $attributes = $this->fetchAttributes(); + foreach ($this->requiredAttributes as $openIdAttributeName) { + if (!isset($attributes[$openIdAttributeName])) { + return false; + } + } + } + return true; } /** @@ -856,4 +887,12 @@ class OpenId extends BaseClient implements ClientInterface } return $this->fetchSregAttributes(); } + + /** + * @inheritdoc + */ + protected function initUserAttributes() + { + return array_merge(['id' => $this->getClaimedId()], $this->fetchAttributes()); + } } \ No newline at end of file From 124f4fa9f769e96760d492aa92ef327b9a2b1c90 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Wed, 25 Dec 2013 13:33:34 +0200 Subject: [PATCH 31/37] Doc comments in "authclient" extension updated. --- extensions/yii/authclient/ClientInterface.php | 2 +- extensions/yii/authclient/OAuth1.php | 2 +- extensions/yii/authclient/OpenId.php | 32 ++++++++++++++++++++++++++- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/extensions/yii/authclient/ClientInterface.php b/extensions/yii/authclient/ClientInterface.php index bd76e3b..c9ea0f6 100644 --- a/extensions/yii/authclient/ClientInterface.php +++ b/extensions/yii/authclient/ClientInterface.php @@ -8,7 +8,7 @@ namespace yii\authclient; /** - * Class ProviderInterface + * ClientInterface declares basic interface all Auth clients should follow. * * @author Paul Klimov * @since 2.0 diff --git a/extensions/yii/authclient/OAuth1.php b/extensions/yii/authclient/OAuth1.php index 3eaf82f..35bb74c 100644 --- a/extensions/yii/authclient/OAuth1.php +++ b/extensions/yii/authclient/OAuth1.php @@ -21,7 +21,7 @@ use Yii; * $oauthClient = new OAuth1(); * $requestToken = $oauthClient->fetchRequestToken(); // Get request token * $url = $oauthClient->buildAuthUrl($requestToken); // Get authorization URL - * Yii::$app->getResponse()->redirect($url); // Redirect to authorization URL + * return Yii::$app->getResponse()->redirect($url); // Redirect to authorization URL * // After user returns at our site: * $accessToken = $oauthClient->fetchAccessToken($requestToken); // Upgrade to access token * ~~~ diff --git a/extensions/yii/authclient/OpenId.php b/extensions/yii/authclient/OpenId.php index 35527e9..6cd84f3 100644 --- a/extensions/yii/authclient/OpenId.php +++ b/extensions/yii/authclient/OpenId.php @@ -12,7 +12,27 @@ use yii\base\NotSupportedException; use Yii; /** - * Class Client + * OpenId provides a simple interface for OpenID (1.1 and 2.0) authentication. + * Supports Yadis and HTML discovery. + * + * Usage: + * + * ~~~ + * use yii\authclient\OpenId; + * + * $client = new OpenId(); + * $client->authUrl = 'https://open.id.provider.url'; // Setup provider endpoint + * $url = $client->buildAuthUrl(); // Get authentication URL + * return Yii::$app->getResponse()->redirect($url); // Redirect to authentication URL + * // After user returns at our site: + * if ($client->validate()) { // validate response + * $userAttributes = $client->getUserAttributes(); // get account info + * ... + * } + * ~~~ + * + * AX and SREG extensions are supported. + * To use them, specify [[requiredAttributes]] and/or [[optionalAttributes]]. * * @see http://openid.net/ * @@ -32,10 +52,20 @@ class OpenId extends BaseClient implements ClientInterface public $authUrl; /** * @var array list of attributes, which always should be returned from server. + * Attribute names should be always specified in AX format. + * For example: + * ~~~ + * ['namePerson/friendly', 'contact/email'] + * ~~~ */ public $requiredAttributes = []; /** * @var array list of attributes, which could be returned from server. + * Attribute names should be always specified in AX format. + * For example: + * ~~~ + * ['namePerson/first', 'namePerson/last'] + * ~~~ */ public $optionalAttributes = []; From b1f6354fea7e52d185e87c939d4360d7dfe89797 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Wed, 25 Dec 2013 14:07:41 +0200 Subject: [PATCH 32/37] Auth clients for Facebook, GitHub, LinkedIn added. --- extensions/yii/authclient/clients/Facebook.php | 48 ++++++++ extensions/yii/authclient/clients/GitHub.php | 58 +++++++++ extensions/yii/authclient/clients/GoogleOAuth.php | 20 +++- extensions/yii/authclient/clients/GoogleOpenId.php | 19 ++- extensions/yii/authclient/clients/LinkedIn.php | 133 +++++++++++++++++++++ extensions/yii/authclient/clients/YandexOAuth.php | 72 +++++++++++ extensions/yii/authclient/clients/YandexOpenId.php | 19 ++- 7 files changed, 365 insertions(+), 4 deletions(-) create mode 100644 extensions/yii/authclient/clients/Facebook.php create mode 100644 extensions/yii/authclient/clients/GitHub.php create mode 100644 extensions/yii/authclient/clients/LinkedIn.php create mode 100644 extensions/yii/authclient/clients/YandexOAuth.php diff --git a/extensions/yii/authclient/clients/Facebook.php b/extensions/yii/authclient/clients/Facebook.php new file mode 100644 index 0000000..7c17c27 --- /dev/null +++ b/extensions/yii/authclient/clients/Facebook.php @@ -0,0 +1,48 @@ + + * @since 2.0 + */ +class Facebook extends OAuth2 +{ + /** + * @inheritdoc + */ + public $authUrl = 'https://www.facebook.com/dialog/oauth'; + /** + * @inheritdoc + */ + public $tokenUrl = 'https://graph.facebook.com/oauth/access_token'; + /** + * @inheritdoc + */ + public $apiBaseUrl = 'https://graph.facebook.com'; + /** + * @inheritdoc + */ + public $scope = 'email'; + + /** + * @inheritdoc + */ + protected function initUserAttributes() + { + return $this->api('me', 'GET'); + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/clients/GitHub.php b/extensions/yii/authclient/clients/GitHub.php new file mode 100644 index 0000000..796b86e --- /dev/null +++ b/extensions/yii/authclient/clients/GitHub.php @@ -0,0 +1,58 @@ + + * @since 2.0 + */ +class GitHub extends OAuth2 +{ + /** + * @inheritdoc + */ + public $authUrl = 'https://github.com/login/oauth/authorize'; + /** + * @inheritdoc + */ + public $tokenUrl = 'https://github.com/login/oauth/access_token'; + /** + * @inheritdoc + */ + public $apiBaseUrl = 'https://api.github.com'; + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->scope === null) { + $this->scope = implode(' ', [ + 'user', + 'user:email', + ]); + } + } + + /** + * @inheritdoc + */ + protected function initUserAttributes() + { + return $this->api('user', 'GET'); + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/clients/GoogleOAuth.php b/extensions/yii/authclient/clients/GoogleOAuth.php index 0d9ab8a..2fcd0c1 100644 --- a/extensions/yii/authclient/clients/GoogleOAuth.php +++ b/extensions/yii/authclient/clients/GoogleOAuth.php @@ -39,6 +39,7 @@ class GoogleOAuth extends OAuth2 */ public function init() { + parent::init(); if ($this->scope === null) { $this->scope = implode(' ', [ 'https://www.googleapis.com/auth/userinfo.profile', @@ -52,7 +53,22 @@ class GoogleOAuth extends OAuth2 */ protected function initUserAttributes() { - $attributes = $this->api('userinfo', 'GET'); - return $attributes; + return $this->api('userinfo', 'GET'); + } + + /** + * @inheritdoc + */ + protected function defaultName() + { + return 'google'; + } + + /** + * @inheritdoc + */ + protected function defaultTitle() + { + return 'Google'; } } \ No newline at end of file diff --git a/extensions/yii/authclient/clients/GoogleOpenId.php b/extensions/yii/authclient/clients/GoogleOpenId.php index 1f8ebb7..cf5a3b4 100644 --- a/extensions/yii/authclient/clients/GoogleOpenId.php +++ b/extensions/yii/authclient/clients/GoogleOpenId.php @@ -10,7 +10,8 @@ namespace yii\authclient\clients; use yii\authclient\OpenId; /** - * Class GoogleOpenId + * GoogleOpenId allows authentication via Google OpenId. + * Unlike Google OAuth you do not need to register your application anywhere in order to use Google OpenId. * * @author Paul Klimov * @since 2.0 @@ -54,4 +55,20 @@ class GoogleOpenId extends OpenId 'popupHeight' => 520, ]; } + + /** + * @inheritdoc + */ + protected function defaultName() + { + return 'google'; + } + + /** + * @inheritdoc + */ + protected function defaultTitle() + { + return 'Google'; + } } \ No newline at end of file diff --git a/extensions/yii/authclient/clients/LinkedIn.php b/extensions/yii/authclient/clients/LinkedIn.php new file mode 100644 index 0000000..c6de848 --- /dev/null +++ b/extensions/yii/authclient/clients/LinkedIn.php @@ -0,0 +1,133 @@ + + * @since 2.0 + */ +class LinkedIn extends OAuth2 +{ + /** + * @inheritdoc + */ + public $authUrl = 'https://www.linkedin.com/uas/oauth2/authorization'; + /** + * @inheritdoc + */ + public $tokenUrl = 'https://www.linkedin.com/uas/oauth2/accessToken'; + /** + * @inheritdoc + */ + public $apiBaseUrl = 'https://api.linkedin.com/v1'; + + /** + * @inheritdoc + */ + public function init() + { + parent::init(); + if ($this->scope === null) { + $this->scope = implode(' ', [ + 'r_basicprofile', + 'r_emailaddress', + ]); + } + } + + /** + * @inheritdoc + */ + protected function defaultNormalizeUserAttributeMap() + { + return [ + 'email' => 'email-address', + 'first_name' => 'first-name', + 'last_name' => 'last-name', + ]; + } + + /** + * @inheritdoc + */ + protected function initUserAttributes() + { + $attributeNames = [ + 'id', + 'email-address', + 'first-name', + 'last-name', + 'public-profile-url', + ]; + return $this->api('people/~:(' . implode(',', $attributeNames) . ')', 'GET'); + } + + /** + * @inheritdoc + */ + public function buildAuthUrl(array $params = []) + { + $authState = $this->generateAuthState(); + $this->setState('authState', $authState); + $params['state'] = $authState; + return parent::buildAuthUrl($params); + } + + /** + * @inheritdoc + */ + public function fetchAccessToken($authCode, array $params = []) + { + $authState = $this->getState('authState'); + if (!isset($_REQUEST['state']) || empty($authState) || strcmp($_REQUEST['state'], $authState) !== 0) { + throw new HttpException(400, 'Invalid auth state parameter.'); + } else { + $this->removeState('authState'); + } + return parent::fetchAccessToken($authCode, $params); + } + + /** + * @inheritdoc + */ + protected function apiInternal($accessToken, $url, $method, array $params) + { + $params['oauth2_access_token'] = $accessToken->getToken(); + return $this->sendRequest($method, $url, $params); + } + + /** + * @inheritdoc + */ + protected function defaultReturnUrl() + { + $params = $_GET; + unset($params['code']); + unset($params['state']); + return Yii::$app->getUrlManager()->createAbsoluteUrl(Yii::$app->controller->getRoute(), $params); + } + + /** + * Generates the auth state value. + * @return string auth state value. + */ + protected function generateAuthState() { + return sha1(uniqid(get_class($this), true)); + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/clients/YandexOAuth.php b/extensions/yii/authclient/clients/YandexOAuth.php new file mode 100644 index 0000000..83e1072 --- /dev/null +++ b/extensions/yii/authclient/clients/YandexOAuth.php @@ -0,0 +1,72 @@ + + * @since 2.0 + */ +class YandexOAuth extends OAuth2 +{ + /** + * @inheritdoc + */ + public $authUrl = 'https://oauth.yandex.ru/authorize'; + /** + * @inheritdoc + */ + public $tokenUrl = 'https://oauth.yandex.ru/token'; + /** + * @inheritdoc + */ + public $apiBaseUrl = 'https://login.yandex.ru'; + + /** + * @inheritdoc + */ + protected function initUserAttributes() + { + return $this->api('info', 'GET'); + } + + /** + * @inheritdoc + */ + protected function apiInternal($accessToken, $url, $method, array $params) + { + if (!isset($params['format'])) { + $params['format'] = 'json'; + } + $params['oauth_token'] = $accessToken->getToken(); + return $this->sendRequest($method, $url, $params); + } + + /** + * @inheritdoc + */ + protected function defaultName() + { + return 'yandex'; + } + + /** + * @inheritdoc + */ + protected function defaultTitle() + { + return 'Yandex'; + } +} \ No newline at end of file diff --git a/extensions/yii/authclient/clients/YandexOpenId.php b/extensions/yii/authclient/clients/YandexOpenId.php index 0c848ba..9aedf2d 100644 --- a/extensions/yii/authclient/clients/YandexOpenId.php +++ b/extensions/yii/authclient/clients/YandexOpenId.php @@ -10,7 +10,8 @@ namespace yii\authclient\clients; use yii\authclient\OpenId; /** - * Class YandexOpenId + * YandexOpenId allows authentication via Yandex OpenId. + * Unlike Yandex OAuth you do not need to register your application anywhere in order to use Yandex OpenId. * * @author Paul Klimov * @since 2.0 @@ -50,4 +51,20 @@ class YandexOpenId extends OpenId 'popupHeight' => 550, ]; } + + /** + * @inheritdoc + */ + protected function defaultName() + { + return 'yandex'; + } + + /** + * @inheritdoc + */ + protected function defaultTitle() + { + return 'Yandex'; + } } \ No newline at end of file From ba1c5d60448311718f1d0702aec325d7dff682b1 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Wed, 25 Dec 2013 15:52:46 +0200 Subject: [PATCH 33/37] Auth clients "Choice" widget markup updated. --- extensions/yii/authclient/BaseClient.php | 3 +- extensions/yii/authclient/widgets/Choice.php | 4 +- extensions/yii/authclient/widgets/ChoiceAsset.php | 3 + .../yii/authclient/widgets/assets/authchoice.css | 82 +++++++++++++++++++++ .../yii/authclient/widgets/assets/authchoice.png | Bin 0 -> 14959 bytes 5 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 extensions/yii/authclient/widgets/assets/authchoice.css create mode 100644 extensions/yii/authclient/widgets/assets/authchoice.png diff --git a/extensions/yii/authclient/BaseClient.php b/extensions/yii/authclient/BaseClient.php index 2741c2e..d155074 100644 --- a/extensions/yii/authclient/BaseClient.php +++ b/extensions/yii/authclient/BaseClient.php @@ -10,6 +10,7 @@ namespace yii\authclient; use Yii; use yii\base\Component; use yii\base\NotSupportedException; +use yii\helpers\Inflector; use yii\helpers\StringHelper; /** @@ -178,7 +179,7 @@ abstract class BaseClient extends Component implements ClientInterface */ protected function defaultName() { - return StringHelper::basename(get_class($this)); + return Inflector::camel2id(StringHelper::basename(get_class($this))); } /** diff --git a/extensions/yii/authclient/widgets/Choice.php b/extensions/yii/authclient/widgets/Choice.php index 3cf2247..c9fcacf 100644 --- a/extensions/yii/authclient/widgets/Choice.php +++ b/extensions/yii/authclient/widgets/Choice.php @@ -166,9 +166,9 @@ class Choice extends Widget */ protected function renderMainContent() { - echo Html::beginTag('ul', ['class' => 'auth-services clear']); + echo Html::beginTag('ul', ['class' => 'auth-clients clear']); foreach ($this->getClients() as $externalService) { - echo Html::beginTag('li', ['class' => 'auth-service']); + echo Html::beginTag('li', ['class' => 'auth-client']); $this->providerLink($externalService); echo Html::endTag('li'); } diff --git a/extensions/yii/authclient/widgets/ChoiceAsset.php b/extensions/yii/authclient/widgets/ChoiceAsset.php index c5fcbef..bc1acc4 100644 --- a/extensions/yii/authclient/widgets/ChoiceAsset.php +++ b/extensions/yii/authclient/widgets/ChoiceAsset.php @@ -21,6 +21,9 @@ class ChoiceAsset extends AssetBundle public $js = [ 'authchoice.js', ]; + public $css = [ + 'authchoice.css', + ]; public $depends = [ 'yii\web\YiiAsset', ]; diff --git a/extensions/yii/authclient/widgets/assets/authchoice.css b/extensions/yii/authclient/widgets/assets/authchoice.css new file mode 100644 index 0000000..e9195f0 --- /dev/null +++ b/extensions/yii/authclient/widgets/assets/authchoice.css @@ -0,0 +1,82 @@ +.clients { + overflow:auto; +} + +.auth-icon { + display: block; + width: 32px; + height: 32px; + background: url(authchoice.png) no-repeat; +} + +.auth-icon.google, +.auth-icon.google_openid, +.auth-icon.google_oauth { + background-position: 0 -34px; +} +.auth-icon.twitter { + background-position: 0 -68px; +} +.auth-icon.yandex, +.auth-icon.yandex_openid, +.auth-icon.yandex_oauth { + background-position: 0 -102px; +} +.auth-icon.vkontakte { + background-position: 0 -136px; +} +.auth-icon.facebook { + background-position: 0 -170px; +} +.auth-icon.mailru { + background-position: 0 -204px; +} +.auth-icon.moikrug { + background-position: 0 -238px; +} +.auth-icon.odnoklassniki { + background-position: 0 -272px; +} +.auth-icon.linkedin { + background-position: 0 -306px; +} +.auth-icon.github { + background-position: 0 -340px; +} +.auth-icon.live { + background-position: 0 -372px; +} + +.auth-link:hover .auth-icon i, +.auth-link:focus .auth-icon i { + display: block; + width: 32px; + height: 32px; + background: url(authchoice.png) 0 0 no-repeat; +} + +.auth-clients { + margin: 0 0 1em; + list-style: none; + overflow: auto; +} + +.auth-client { + float: left; + margin: 0 1em 0 0; +} + +.auth-clients .auth-client .auth-link { + display: block; + width: 58px; +} + +.auth-client .auth-link .auth-icon { + margin: 0 auto; +} + +.auth-client .auth-link .auth-title { + display: block; + margin-top: 0.4em; + text-align: center; +} \ No newline at end of file diff --git a/extensions/yii/authclient/widgets/assets/authchoice.png b/extensions/yii/authclient/widgets/assets/authchoice.png new file mode 100644 index 0000000000000000000000000000000000000000..ffc3c5e7ec3298f40e08c6ec0fdd27c6a372f2ed GIT binary patch literal 14959 zcmV-#I*`SQP)YC0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBVX5lKWrRCwC#eRp6S)wTC;W@r0qn|hZe zxp#_fT)=dQX(4pp3r<3SyhljL0|F#~NPrNMKuE$%2m~7lezb?dbo>b26oY#&lB`}< zWqY6b&b_-UX|+XbVVgfb4_vQycIKXQPd~Q`h+m4LV-_*qula6l?fewjfsg$1OeM8Am9f47+R(A!L70(3fD z&0d$k;osVtpBzHqkyS6Qm^ra-`Ly~8FzWT#yt@yAK?k))HS}V@8$!8NjUu~#=w8I< zLErj+!9dTcBsJ6)J4CfMD*~=PJJGc2IUM+6$I2_38owv~;fZIinmcvU@|lzBVKV6P z!M1Mv^@|>ql~HBF2+4`aIIyP;PyOh4SPXGF)!NZM5JvCge}mPggR#g0(~QYk5wMn3 zV)9j&Bj|o?`HFD#eaULhYpAcTNfObfDYeNSvU0gGCzvk>^ zc=gu1Fv+Tg%4LLFj6h}0%ZNavqXp{1VpJ_T7F~N<8^q##dyPr29|4KT9SkEFiX@d# zI0BtPivX*e`0T@C)=yg(!q}OS=pbf)U5F`Xb zDk)WBqY6H29!$A!n?DcXOoZKqmbm}x=>YU!f_>fd^q4lk7pPxSi@BHpv zbWw$(T8$`S8C7IN!(oU--m0QugY)sP?}drXiHDKjr63S;Wdx|v$5^e_h!9i))k_Hc z?Nzd|x_l}koJS1j}>>kmU8l%b}E3A1lot&|uoc<#nUNl38kFKag8Pj75N zbFUlK1twf|(q#PdiUs_)pFDjpo^5>(_Cg7jMg_A=B9OAgR}q<7tS~te&_AG%+5)Zn ze1yOqYZPN)8QF9RC^i_NpHPnXodG<(Y6C)UB9A|a&+k1SvnCV_z1Y&%1(i-kCYS8GPJu=>en(6) z2r&{O;UXl$WN!7OZWc*{JTkAM-GYclB8gA7>u1kef@o&|10+v9UI7lDFfQ;+wV6za zAp$IuL0|~5qEcd>j|I#!*?^C>CENZPl@syae>?_#r-(kEf=^pCXdhG|8Wj$zQ04SV zQ2eA`Q9{5)K}V;{i@m-5@C*zh*z2GlgSh8^zDU8IA+h21XE7~Qj*fvSy`w^FKedP? zJJ%vdRUpDc3edk@P8aqNU^^M~^JkV|=E5nMRg=$oZ#AV4Ff4{VtiJwXG_3wJ-spM{ z2BH@82L&cM#RjA`N&-=UWKR+TA=IiPxb5;exbmcl8DH?UZiA_EMhX%SU3oh^k9+WW z|9Y6!0$d~$My)a?0tz8Oh8EI!(+ct8oo9{+vz%~jehaRh?;zT@i(FmjkoojR3Q_mn z$5Iga{S~+2cOBZ-+yb+{k77Ry1hbZG zc5U2$lC$v*p!Lb~VL$yo6wkjji4xZ@y$XNc_!y$3G|bA4i2%hQAx43OWAk=wL(d!6 zqq^P%(J+nFlz6IT2uKj(v4QL_KvkX@eH-pZtFsN2XZ+Vtg$wHE;(?E&2!uuAFFiXJ z0;C0;0&*OK-Y=d&*#sT5g_RJsIx4Bcrx?tLk`XGQ(5!pk<8U>e0CVj^{@b*QiHHUj z_{csO$kUA_hYHhz7zJVoNOBhixdy1T5hij_f}ZpSgoq*L8BkJZN85%ch7f3Rbdq6F zc$gJs7G^9A5)?=vATz5F=qCuF9E>P4Q$c9NVI)7^wSgNhEn&c_8uOAxj;Q3P@F_qJ zNn3R~+LAIiz1U!ZSMMS;* z{mtEd{WTV|2@}8j0y^J&5WSlohs`X(kf#vOhp7<_3-GxqpzxVdwCo`mN~UAWfjzk6 zrN1EGs)CO6pLw{*7zDJbZP?r9CZ{7eOHQ|^VZ*kaD=j7?iVO2m`K`MVSa3D^w!Ve# z&g~?RZNqPxDqxzr5cM<9qKmZ)U#ekP3pg$6r3P6b~z zb}&pExUAm&@ZVNUt*c!=Ysy59gifoWk~zuTv41E2_ubXl-M$xkvj+JE5_Fn0f#Aqq z39>q@H1+gjeN!vEzQD@ zDm&5bbP#JqNMxQhIu_yP9tS8%aTgXUoHDNb?PqxS`JMck=ihIIwYG>{vY1j-S~Z#}{NC2#O-}(P1JP)9A>8i>BSL`$ z%b+El8ak_ym-pKJ*!2U-V`!*zYWk%E;l%mzJgZMN(E-Gg}LZ3m{78gS?LXYmG$2_a-QfKcGJ zAYG}nYfwBmfN*;sr-GnT4Hd0P2c1QqbT3`enhhwbwo}p~M2rL3*dSW- z5OtAY8Fk~b5e7=zr;>KmTZ^!CraeXhMTi-qs2t@i$cDgGr&nO1BZL<>4Wc7(@J8!f zQ}eL2))3=+A{AsX6NX4ZOe(WowRSb!JKNyZ8epo-eTY#n2{^7on^M0bDqIHQ6aeoF z?(Or#KvHVdOKB^+^5Us@tucrWPc&x)&ZyV(t~^$M@(7H`DIjtgK#T=;yAIF2-Hi9X z>fzrdC+6WZu zhufBGU_DF$WVZvXW4hkVN)~<|w0THa!bogPkOhhI5ef?>3W*s^2Ai`WM1N>R))5&w zA_(XNiNj>D#b`Lv+|uoUnSzKI6%K!4uoz4RTa=^GyVvh-g4g5aBPV)E#dCyALCSaw z6ERjdgTY{}-_yi;=I-X%$T`1XvBG51FP}fT7PX~?P^%@V(~O*cohuv(BTRg6?Cizo zyBgu~`d7Yo)AxpZNR~bDkLBbc?@~muCYH1x>P$)D{|mC*EGW49^;>?h@@Vf!5}h~h z+Qmkv8mJscrSvet>!z}q%6)UEOz9XxfPkM&A1`RN+WaD$mBMlTVS(-s1kmqt!Rzy} z;q^-ii8VsJpc>r3p_{6%j zl`A7KP{wN1ND451D5~rcuc~c$6g^s8oR+`J7~eY;ME z`Fr7-DzvP77d?Oe{jfDvvb;Yc=8#6}@4=*o1vmd<$T2y7eJ8@zRZ#Wx=IjlHk%ow2 zldI+8PHF3Q7~g$=sP*2HPfYRuih|rNVNTv_#8vduTZV+K_uht~_YTG#CnJzXI5Nfr z@4O4`?!ElkV0|sTCoW3se3TqnDjlrzC??(~THk%oougWf+jmpms~}NK zz*|3QB77&zPjWyx#{==s>QoAh(Xmu~*S$ln@4WZUl;@L$Fnds%m@do^k)1A=!fwb} z%xB?me$O)7ScWrg3Vg>MGj3mIVWwF%j>Z5xZv+AhV;JU zzJ|0GE~%4=%yhbESp5u^N~K~|ZD6U{B>H0qLoDBB6TE&uBJs}4L!AW^P_T*geFFo^ zpvNOvO{OJ@$^U4dpW}P)n*Eryg7X z)|IEt`GeJ@4+sR@e8)f5e!6LY=QNXEbJ*t*Fg5i0Lzr4!wBw#1oPL6;cELG!t^IP} zg=V8JX)(iLBRRp)swK2LT!lWbFRD`4E%}Qe3IzunmVMpAx@y`T&LYVliWaf?9GM-* zc>CSPzJd`HNux8G)b5lhqGL>vRN|0Dl?V;*YtES;u_>LIS|5$dIDJMTYD=vd#}b%O zF(h4q2-wK``#2T0H+A9hmp`KTBb;J(Sp(#3^~*|32oIJ5OwB|=;bvPRa=X1= z1Syb_@`i=3xodzU%BUb`sGm_l%GO7i=|wgX+UpDOrCnN;jFV^7!C*)pPk#Qr&D=uB zSt4z|+Ado+ijsjkKU{V!&X`ldjZRrX9%fCc=d6)>a$;@ zY<$mR4`pr9ueGw8swop#UZV9;=&cuSKa?q~e>?OzG^c`FY(-# zjWrEA*);I7QOHWMiAx#WX-o)_nFZ;72~dlCmQ75ZTyDd6PCX9x{A2|79O%Z*=5Ee^B_jpK!GfEw zT82m{7;`2p)t1uF9sMf_tB1_7fuilSEQr~G%oLC_cH7X4n#}Xc7|-un@ita}umwh) zCUeNAa2BLrlB2{appkN4JU}@HKH1WUm9Kw}Pd4v|ky<`huZzS2l1%1~EeU_@we@)I zi^k06nsr!^Ksr5sboIH=(%sL28g!Z*`ke$eheR0~aZNQ;0zTi_h6p*9u_AGitQyPz z8O4Nnzhf?`8f#=vC=#`2vez0(E;hMHJ=@Y zIQekpsEq8AsO+9uX?^o&mo5F>(!~n``2YSIBKE=y?zl>=(Kb*pQFAx}2K@f!a46WY z?&V((??bZa!n;<~RF*G4b>VDOl-Lhzi`%;T@vl#}qN$~I zGZ)Q8pF4=wZs%b|pt9J4{5&n*{OBvRb+j*+B(RR!Cd?Y7uWiPv*S6$@l$7X-8P8=iYxxS@(bw8_ z`*W&usFMmx?!&etul~URg;l94*VXIJsnc4HZ3iUtKA+Gy*`P_&8JWx4HH|3n1eH^O zA&{#a-2c=0WA>ulcf-7td$<1a^W=zILV^McR1k8OgJY&o8Kp|fn9sxn?z801ObBrq zP-5L}N{$qe<>Ad81dg1SPqrZD#oD`Ul>La?xrhs3qfWejoo}I)rAvz9!Fdk2;WouFqn@uIf z1|DQsbZYebLgNs^gSU)Et;At+*rFV19&kBPnZp9MOhP!ijF}w~EC!Ro7UiI)VP9Jt z^kM|ZRN7Ezq9EfSHX6$n0k*h_)y-fqSPUkE&6Cq(&%0xVsi1WEG)k9B>~=oQMpWg7 z#P`pyO1byMD=lfYg(26~-;bTmEpR(JSAMnT*Tbu9jy?Cbt3<7_Au32Uqi5t~yMBJf zw3K_lU9}@kk6Wx$Tct#r<)F9Wt5<(>bRUxB9+H$?Zf3^$fZyYX&&!Gr zJ~#inDHsU-=;57%Z;uy&6j8svK5vd%tA4$*u5?Px#3~p}hS(bqiA6-pe^!EnfrF1OpSHU?bqqUihn#bl166LIsP$)6?ecTAR!=b2LH_0v* z*4g;%XiI?>7K;`WW?0FI1ChZn*>IwW%Z;5|yRr7o7PPem$<{|Ae{BxPUyB7kU-W=Z zC%G1#QpN`!ltFT}ZmQEu=Ie37(qdfoU7}QB7#_CG^VhWl_b#?u z7B8}{J@Kq*$16I@`}Ha|sucT?#hD?6^)5k)1n6(dfP)T*q%9p?Ml|^sy|?K5x4uhH zv9UY0Af=Vcb}GAxJv})aBKeh>k3m;-wW2M6pfw^EGaXQWJNhFiFsm@3SdG~=I!|T3nt7&|sjMJGy2eBx^1stAhuXYK(B}i{ zJP0aH%#CpuNvsItmN54eR#;+pO0^OlTPlV#9$R^bYv3e5nO@T~Zh{>%|)hiN9v`1>vV_7YR1o z6N1V(NfFe9gqrhQw9iCf&TnwXb4F}v>O$GVPcSfHEqV+)5Ky_G3l$?TPy?H1Hu4AO zLhr7F;w;C`b|*gC)`L@~YO(T$auk|X|B*ltA#y8VlDVBB$pmo1kLxO~%B=pQ3!GW6)RaaaDy}lP>&Dr=UbS>_9ejgl{Uy1gT zFCc|7`*S}3oVaHjn1qG@`UHxn0L?uftl!jz$8RXX_fD~{rbjN5lwS&V5|&@mmupzW zoOs)0p~~9^;c`!f@mr6PkOlGlU!n*ew+cO_1U#61>Yz*-Z1&qRbMG&RHFh|u&U~8* z6_s}Uesv!%Uu-!?rxA{4n?kLETDp;nkqUT`BuHin7E%P)N2|yf^@9bk9Qlw`8!ERk-Ek=MF1tJjGU7(*BD<-$MT(gLO`We!AnLh z990B{KqFOFo3W}wLeQ*%$>@eA6oKT6Pwx;%6k!BHjM4fo$R-EjUmSCH%!raNAs>c` z6gPcH=ACVnB64@Em+qtZ>hX-t2@}1QK3IhrPTQ)lq8SdZ(a#n^= z3>hKD2DX>_+(Nc%hl~L-*V9Va9tT2*XeB-Yon|h@RsWeblpujZU^nJk&O=G#Op1)# z`K}zwsKVIAwue=i&;^UgEEhr1$E-L7)v(h2)9ctyk0JDudp~X>A7A)f6qfB`;ml2h z#Dg&Ef?#_NjVyVDfM+94s8mqty$phjNJ)W-+7~Mt_ILcdh82$nn!#%zsLLUc90W+l z)l|hYVsahDG@tAZK&ugP-fW(YujT=XaLOhkkZ=p7#Xo>vvzH2n3R}V#ZJ~^IJ+7!* zhG^GZNP##4UZS`+_Q{IMHxHtF6~yRuy!Czp&ZZfjTLCOC19oQ8XIHLINs{7ph~Ua(KC!VsZz}XlM4uVBQTDwHYfyV za(*Wn#+z4cTd#e7$0KN!Zig*@5+M+QAjJkk`@E!X6x6CHkUg(LgH6B_wj^k|-{YLYtrHSy2mM9^(@#uT%Me0Cykl4N2c=tti5CE&huHL17CCXCI<^ znR)n(oYak%RTyBUN-uv-^(|@tJdTpEnY=^X1(U1$hXlnit zaCQ~2eMO56cwt8m_V)YG?G0g5mj{o2)qzg8pCg#=I^YaoWZg+6B0u!}^NK1)ff0jF z_0$?unxk3FYzjXK>oc1>Q+`{ss|Wu!yOf`Ml(+^15&Ua^U$z0Q>DvnTT}Pjf5DSLm z1EZALCbQq7X@+E*`aF2#(`FoBVTGNjS=%x4yPzRT2db4EQoWePFTU`*pbk90@JifqC@VMC^KQOB-K>m*L8%jO-U86O=^R+vCF4&eREw8>mH14i82? zJF-;dY4M#IWgK|onqW8J(plwvEmEfUWI6gBi3!EX>#6mPSTvy!Q;N-T5_|F9-ma{J zSaZuQY4_2YU^l|5Q^DcQ$PPtHrz#)s>!uggSW;JnDMe;1tuMl>TiftyQ*YW@Oq$F~ z#r2YyNgWx|&x`5QBh2Nx5fPq|6SDd!O#YB;i%>Gh}26arr$SjAyMuAH@?a- zp}>tVTyjY?77-B(Mwe$l+mq5)!PcIx+1LnUhSjuIqE5QHQc75U`SI_c`mbSU6iqc0 zU+nLI%NM{|vnHU>tmiF%`Kx{Carw|O;6Ztsqa+)929d6=(NjlAu5Zff4HMV))?Qvl zUr&Fu9XFpdZ%9Mf#ZHBK?$eza)op};Km7Fd=k8BN;2>u}Y-;Yp9nXJ=WyjQ^#9~5g zp9`zk??JcIJ4POf9<`{sCp9D+#}tfxoqc%t^@BSnRZxUqKJ(LWr5Ft9NbC+vq~AQv zz2zV2hwYBQxdjvz|Fof@A#GeI68H_dY=|}9LjSIQ@#c%)&zLkRiu{|zwqS>$xD`>n zDeLh4&bOc6Ipwn*+v({^#I2G;mYxgBr&RqFFa6hdwjb>U$*Li~UrlCHqwl1$oJ#o- zg3GppSxMzrER?yM%B@GZ;jE!-Uojrc|KJHxS+S+QA}F$YdaAQz#Zqg687CK(JS(AFk`%j?fF&LvePFZvsPBX|+~B^Y1G& z0_T;LPKWlpPeU-+4?P0vvwoRcF`LMsL{kt|pLX-1MSziBHF4<(I~PW1^-b{qW)A%K zOhwA3&gTnu^So#S2v}1A3@iosAhEU`#IZ!oYgS=#@#Ws8%g%U`V`=buOH#J?m6^! zfKeg%&mTY;Y|H#i&mM%HUY_-tbnf`zqEaHidhvBo%{Ui=C6*^DeNBjLUd`2!*@%OU z2af!4UI!(FWK`7Ox(ll5XG3YA7ymLgG9~|Q99tE=l%nsHMgZlBCq(+ zA)r3}*0jK*`#(aIOmI3R)F=P+(96JR;6u04AOT5x=;DF!yT5~4e-8wzG(@g;bbvC1 z1Q{L{5i?C-@8qh6Jve;(l8$xtG8X*f8+3*%84K=WUY^PoR7#EzU^c;d6_sBe?I9WJ z@GZFJf37l{4GjhPhxfE!N1vzJ(w&g z!;4=wW8J2fl^u^<{5_%IhpVnybj*a6S1g!_-VkuW6+Wy8RGZZ(B$0pW<6ZcCOUrU8 z&!lUZS670s2zrO_@Sp0{=?$aIAYtL;a_ngAX^?DIL(M=~L`NX~;1U~@a`|H8erAJ~ zj}d*{tIZvPtP{atOKhLHZfd!l#x1lt+)zutiykojW#q1Jglo5DThP zBcC&;mTp$rbdVdphvbN223wRf9W$#{$>9#W~XplfM6bT^rg4qbM?`Kc-CKyYwpcWM)9%P+Wh#WEv{X5?}a~)VKr!d@GJPIYt*Dga1C* z`HwfBQUiy}pH#OLrzW3GCuZ0fFWok;>|VP2PJaZyd$$P}kk)+Cn7oz1k~$mMV!V-c zi}9U4MJ9kMB|QtIG_%;)y>$1N{P7DsztMpKKXC8cO=;g7S+^>FFazJEQ$+e5Ii)l$ zl``y{7m6sb>Q%g+Mu$J`dm~}QV8*urVbiqI8J0{GNSooX4HV*aWO{ET3_%g&d(ki! zWT1db5OcbhxSyya(|aRfsD>#Z4+&vPQkteE>I@W6a~{iC-x~=d0aJ>`Pu58{xi#Hs z-a-Zh*!@VH#F^fUjAQ{~8>Vn)q(b`LQ_{60D*_QE>wAgc6D)`(gphp+%ef(tae^t` zPt=j=z3?a*7}_?R@gtH(2E;U;Ow8cj*!@Huncj<}lL5{bwhoJsi;QAJC?aG2!^r~= z;rImCboUc-lfE*RV04uH=BARM`5A>u{W5U7xcM&<8nY_y=P@p#pKwWLN=wd z+2IZM~1eq<^PUaae!}LK~H*D=RB+PeI_ofdkv)zveG5NK*@`VD>DN;mm~(fH0ehw@^km~AP|8b*y}x9*=XVM6Rgr+xeOwU(Ec zSFnfV!@OZ>0!Dj#ds}^deWe&rktz}ngzrT(syzMl)0j7J9v*t=A@ue2aViCaL3lhK zxZQ4caxX%m5HIh%^A0Y!-~xRA``^dFz(7h}Bao;V>a0$jHS0E6`*rLS6sFJN- zW#l=h2@0eujO_2e`|iPaLUniJ(MKQ6c$`dv0z(L7Dr?rP!RMcUJ}!xW?6JqNckkYe zb&n|nOsYTnXn1I*sHh0%oO2FJOH1K!IMCnU&t-ufqLQDV51Y+~J$v@x)mLBTGQfV@ zuweri&@p8oo&GZ+Vk*Xi2r^PnJn;l-YiqfB)z#H;1bK$bjuR(YX=!QU$eexl+4%8~ zf6Rr~>2#*8J4FW46=t>a@?x9dUU%JfIQH0Mamgi@;H1 zzWQoz2hKb1JpLYge@qr61cj;GNQ$ugH8ru``>LucE|5ES?&K|Faxr!4)S(tJgnT|9 z|0L56GPAKb!)oRC6RIl24_8dG0||gk&l%q$@#Q?MbqOHMxSV`2;V&{4#F8{dSH}f|^Wb5~NSj z0#+;*3r8S<9eaV1>yA6_KtVwP?!W(jEL^w{ixw^7$O-ZDjF^95?=u7Sw=Yp=aF$v`oRB;YYENv7w;>mtJ})e)OXsapBsuX%jy9-~+t);)@(X z#wG@Yk^16`FUG`)6Y=-I{~hFAIg7IO5sqmGCQX`zYp%HlH{X0Sm*!JWIR&Skb{gkt z+@~j93@Eb`tPwt6_T(fdJHYNOUAh#@mM!D%^~x)+lEpf8~`|hE(>16HefkkjVg(Y=#I!BG-T>tsTg8G2#5vpZ=)v|==%XTsK zW8`Cqu#4$CbHD5!V?|+MVUjURcQH0Gm_rDV5UnClcN0@7olcka<%Gvf#0l(51g*JT zOnaDDWiUtfzN^WIY@wTzSs0!0Vu$i#lwv{~XVLC&fBV~Pf@+unXR{})RJ1EsuKXnd zKA$3-VjPBx$lgteys~`xayIFdjRy}VA#j8<^C1K{3oxEJMPHL3j0MulR#_;};DmgQ zaHCGdQo_b^IH8h)Is-D^?+YmpIVk9s2>6L*TKuWjfSGv`azCSVyvDhFtO4Mnz=yHXzvuyyMw6>$e@g=T3`?wS&`1ZjBTT-&@a>h1B z{k-vG>~d{}s1wld?nhux7$?@8iiHzTCgDqy;RdiqXE_~F!7Uea?m{eB}d#M%e!4^EdZUuIC?&8n1 zIh;^a98hR6#13LmVp~KePW_8064vi-M<5tZs}f?iLnJq}tcThr!eX+b-|s-5rw5T} zh*;8zFZQjAnb<&(AEwGqqX|T14EQ7H^M>IMgb^X<6efY}cY63UsqondY8DFgdmM0! zZnO+EaXIL9^djI5@nc5&-Lc6H?CiRJ=KvbJJ$wV*ZdZupB>=zA4}}CV67a)p)ufO= zK>;Zn1@eu>*wV5AL29Mn@8jFV20dYDL=DPpv2}5kg*GVueOR}-2?kRh{Ct5kCAQ3K zvcx9j#e!**px0^AQXq{C#JAEz6!UB6LF?0D|Bhz#vh-6i402OoYR=h(+#R!rfeok%`UQX6bSeD6%6{qFlfv; zY4S1%@^tL(4MK0u!+ z#jf2@`adI!Kmmi6WK*MuD5P7(FQT^`YA1hTSK^RyhrY6RfZqi49~|B_jC{LkSsgFv5=^ zJ!#GaJiB%yKHS!dZ=X<4f)|KKgKR2*>?SnHs07+bSHo!Q9)Ms|pwR^(lFqB;Ahc=` zYMqI)A3Y>v74kK1%oPVPLEj8VKuuyZ6N0XWEy*I|Bk`$W`$-sEdR(wk$Zj*~P*+)m z|J%}tGpDIw3N}&*bPUNr9y{xfu<`e@Glbz%Rb!(%jN-DXguNd|3Q|pM#zepk$&HGzXTS>-7U5G+^9yV|7 zN8k2;!erIO5a4uBcxJ-(_`~k734y{q^oFC@-Lj9QeG>W360$aS1oc(~2D*q2}VhScq1~Ure07C9{6dmt}!!R8_ft(d-RVYG6ske`STOg5^81*{raoJHlYbm)x zihEpa+)&U%(G`<4@526`J%|>}L~myY9D}{6id2(8g}J-+5>K5fHF||S?Cs2heXts3 zd-vh^>M-gHMC55zu!Y*7=_gH!l%m&LjC?Zm9+!_o@qB0`E$mhcy2V2Bfm7pcCm|%! zBA!ntY#QNf+d;P5h31xK;$x5sS-xm&-|}FD{*DCU^hD4Tw4obi*zQ%K*J8v8h7cx1 zx?!VS!yx%lE4IQ)$P^kB?Dp6w1R|9*m%?sThK7Fm4qJTqo+togHJP__fCC-`B0hNB zgY3W|_`+Tem_5q`G(u+3@0Sq>YxwYYNHL&Ks>A_<4Pj##`d~M7)DTm!6~^cwCY$;( zRohEGp%X4=DNKqQYCjpU-Xg$IK*mnXd;!HrDz=s4RI*ytXzJTeScae>elZ@hBGP{> zFku6a?3j7B`Jou!k(n*?2~ovPQe8J$$56Bgu0RbueQi)VTag#;Lrtj#`C229+z**8 ziXcl%Ars%yQ{t9b(C~8ltn<lfZ0V8(BU3HzU;$UMbr4gD8&;@&IQWs zRrukFKf%O;={!paQ&ar`5~5Im=Y&z>pUx0PN%=0Kz$OHJ{aijG@kx=aFdhfPtO$g> zgu#G>Znp?mn2-r+;SaOWn{7iKGeqL(RN88^p@?F>a4148_K{Br#`=E3Y@!1>7Zpjf z-HXy{LPF@_L+~L&B0$fxy-I@g$@aPDqa}Pgm#w|N=>EdZPfANgu{(*Fz3A3u_;uihg_;l|&@^B6=3$a{^0u4Rce7%H%NoDwQ zmk9&G06P(jJ0FHjn72GOJBtn3v%pnQlRqRGnp0nj(tLB0JC-sGv0_4T9WU9g^1LQI z@!k$}w~)Y^jXVeq5du*b3DKvDofgajRdS2dtMjS8)ck;L4dl-Pzt{@BC@C*Euvc-7$#D@XcbFd1yI z%M<#;)?V+nK7V-J$4VVsa;kvAVlWwOsm&AkkfPHc`@_*24QkcNY?S7(rsw3JeBtPO tUEc76?VjL=f}(t*OHT2PBk=zO7yy_}K(5un!x8`h002ovPDHLkV1mq*w%z~$ literal 0 HcmV?d00001 From 6b3745290b0436d4572ba32e65c502ab65aad198 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Wed, 25 Dec 2013 16:01:48 +0200 Subject: [PATCH 34/37] Auth clients "Choice" widget javascript advanced. --- extensions/yii/authclient/widgets/assets/authchoice.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/extensions/yii/authclient/widgets/assets/authchoice.js b/extensions/yii/authclient/widgets/assets/authchoice.js index b0aa885..ed59e9a 100644 --- a/extensions/yii/authclient/widgets/assets/authchoice.js +++ b/extensions/yii/authclient/widgets/assets/authchoice.js @@ -1,6 +1,4 @@ jQuery(function($) { - var authChoicePopup; - $.fn.authchoice = function(options) { options = $.extend({ popup: { @@ -17,13 +15,17 @@ jQuery(function($) { }, options); return this.each(function() { - var container = $(this); + var $container = $(this); - container.find('a').on('click', function(e) { + $container.find('a').on('click', function(e) { e.preventDefault(); - if (authChoicePopup !== undefined) { + + var authChoicePopup = null; + + if (authChoicePopup = $container.data('authChoicePopup')) { authChoicePopup.close(); } + var url = this.href; var popupOptions = options.popup; @@ -47,6 +49,8 @@ jQuery(function($) { authChoicePopup = window.open(url, 'yii_auth_choice', popupFeature); authChoicePopup.focus(); + + $container.data('authChoicePopup', authChoicePopup); }); }); }; From 52de0405f932d0af1558b747977acd696c686ba6 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Wed, 25 Dec 2013 16:13:33 +0200 Subject: [PATCH 35/37] Auth clients "Choice" doc comments updated. --- extensions/yii/authclient/widgets/Choice.php | 39 ++++++++++++++++++----- extensions/yii/authclient/widgets/ChoiceAsset.php | 2 +- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/extensions/yii/authclient/widgets/Choice.php b/extensions/yii/authclient/widgets/Choice.php index c9fcacf..667e2a0 100644 --- a/extensions/yii/authclient/widgets/Choice.php +++ b/extensions/yii/authclient/widgets/Choice.php @@ -13,7 +13,30 @@ use yii\helpers\Html; use yii\authclient\ClientInterface; /** - * Class Choice + * Choice prints buttons for authentication via various auth clients. + * By default this widget relies on presence of [[\yii\authclient\Collection]] among application components + * to get auth clients information. + * + * Example: + * ~~~ + * ['site/auth'] + * ]); ?> + * ~~~ + * + * You can customize the widget appearance by using [[beginWidget()]] and [[endWidget()]] syntax + * along with using method {@link clientLink()} or {@link createClientUrl()}. + * For example: + * + * ~~~ + * + *
    + * getClients() as $client): ?> + *
  • clientLink($client); ?>
  • + * + *
+ * + * ~~~ * * @property ClientInterface[] $providers auth providers list. * @property array $baseAuthUrl configuration for the external services base authentication URL. @@ -122,12 +145,12 @@ class Choice extends Widget } /** - * Outputs external service auth link. + * Outputs client auth link. * @param ClientInterface $client external auth client instance. * @param string $text link text, if not set - default value will be generated. * @param array $htmlOptions link HTML options. */ - public function providerLink($client, $text = null, array $htmlOptions = []) + public function clientLink($client, $text = null, array $htmlOptions = []) { if ($text === null) { $text = Html::tag('span', '', ['class' => 'auth-icon ' . $client->getName()]); @@ -145,15 +168,15 @@ class Choice extends Widget $htmlOptions['data-popup-height'] = $viewOptions['popupHeight']; } } - echo Html::a($text, $this->createProviderUrl($client), $htmlOptions); + echo Html::a($text, $this->createClientUrl($client), $htmlOptions); } /** - * Composes external service auth URL. - * @param ClientInterface $provider external auth service instance. + * Composes client auth URL. + * @param ClientInterface $provider external auth client instance. * @return string auth URL. */ - public function createProviderUrl($provider) + public function createClientUrl($provider) { $this->autoRender = false; $url = $this->getBaseAuthUrl(); @@ -169,7 +192,7 @@ class Choice extends Widget echo Html::beginTag('ul', ['class' => 'auth-clients clear']); foreach ($this->getClients() as $externalService) { echo Html::beginTag('li', ['class' => 'auth-client']); - $this->providerLink($externalService); + $this->clientLink($externalService); echo Html::endTag('li'); } echo Html::endTag('ul'); diff --git a/extensions/yii/authclient/widgets/ChoiceAsset.php b/extensions/yii/authclient/widgets/ChoiceAsset.php index bc1acc4..a30570a 100644 --- a/extensions/yii/authclient/widgets/ChoiceAsset.php +++ b/extensions/yii/authclient/widgets/ChoiceAsset.php @@ -10,7 +10,7 @@ namespace yii\authclient\widgets; use yii\web\AssetBundle; /** - * Class ChoiceAsset + * ChoiceAsset is an asset bundle for [[Choice]] widget. * * @author Paul Klimov * @since 2.0 From d8079cc62f4f1264c0c3e735d636ba2b82c414fc Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Wed, 25 Dec 2013 16:46:13 +0200 Subject: [PATCH 36/37] Doc comments at "yii\authclient" updated. --- extensions/yii/authclient/AuthAction.php | 121 +++++++++++++++++++++------ extensions/yii/authclient/Collection.php | 2 +- extensions/yii/authclient/widgets/Choice.php | 8 +- 3 files changed, 101 insertions(+), 30 deletions(-) diff --git a/extensions/yii/authclient/AuthAction.php b/extensions/yii/authclient/AuthAction.php index 7584b0d..0935ae7 100644 --- a/extensions/yii/authclient/AuthAction.php +++ b/extensions/yii/authclient/AuthAction.php @@ -9,13 +9,44 @@ namespace yii\authclient; use yii\base\Action; use yii\base\Exception; +use yii\base\InvalidConfigException; use yii\base\NotSupportedException; +use yii\web\Response; use yii\web\HttpException; use yii\web\NotFoundHttpException; use Yii; /** - * Class AuthAction + * AuthAction performs authentication via different auth clients. + * It supports [[OpenId]], [[OAuth1] and [[OAuth2]] client types. + * + * Usage: + * ~~~ + * class SiteController extends Controller + * { + * public function actions() + * { + * return [ + * 'auth' => [ + * 'class' => 'yii\authclient\AuthAction', + * 'successCallback' => [$this, 'successCallback'], + * ], + * ] + * } + * + * public function successCallback($client) + * { + * $atributes = $client->getUserAttributes(); + * // user login or signup comes here + * } + * } + * ~~~ + * + * Usually authentication via external services is performed inside the popup window. + * This action handles the redirection and closing of popup window correctly. + * + * @see Collection + * @see \yii\authclient\widgets\Choice * * @author Paul Klimov * @since 2.0 @@ -24,14 +55,30 @@ class AuthAction extends Action { /** * @var string name of the auth client collection application component. + * It should point to [[Collection]] instance. */ - public $clientCollection = 'auth'; + public $clientCollection = 'authClientCollection'; /** * @var string name of the GET param, which is used to passed auth client id to this action. + * Note: watch for the naming, make sure you do not choose name used in some auth protocol. */ public $clientIdGetParamName = 'authclient'; /** * @var callable PHP callback, which should be triggered in case of successful authentication. + * This callback should accept [[ClientInterface]] instance as an argument. + * For example: + * + * ~~~ + * public function onAuthSuccess($client) + * { + * $atributes = $client->getUserAttributes(); + * // user login or signup comes here + * } + * ~~~ + * + * If this callback returns [[Response]] instance, it will be used as action response, + * otherwise redirection to [[successUrl]] will be performed. + * */ public $successCallback; /** @@ -42,6 +89,11 @@ class AuthAction extends Action * @var string the redirect url after unsuccessful authorization (e.g. user canceled). */ private $_cancelUrl = ''; + /** + * @var string name or alias of the view file, which should be rendered in order to perform redirection. + * If not set default one will be used. + */ + public $redirectView; /** * @param string $url successful URL. @@ -120,7 +172,7 @@ class AuthAction extends Action /** * @param mixed $client auth client instance. - * @return \yii\web\Response response instance. + * @return Response response instance. * @throws \yii\base\NotSupportedException on invalid client. */ protected function auth($client) @@ -137,12 +189,20 @@ class AuthAction extends Action } /** - * @param mixed $provider - * @return \yii\web\Response + * This method is invoked in case of successful authentication via auth client. + * @param ClientInterface $client auth client instance. + * @throws InvalidConfigException on invalid success callback. + * @return Response response instance. */ - protected function authSuccess($provider) + protected function authSuccess($client) { - call_user_func($this->successCallback, $provider); + if (!is_callable($this->successCallback)) { + throw new InvalidConfigException('"' . get_class($this) . '::successCallback" should be a valid callback.'); + } + $response = call_user_func($this->successCallback, $client); + if ($response instanceof Response) { + return $response; + } return $this->redirectSuccess(); } @@ -154,12 +214,16 @@ class AuthAction extends Action */ public function redirect($url, $enforceRedirect = true) { + $viewFile = $this->redirectView; + if ($viewFile === null) { + $viewFile = __DIR__ . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'redirect.php'; + } else { + $viewFile = Yii::getAlias($viewFile); + } $viewData = [ 'url' => $url, 'enforceRedirect' => $enforceRedirect, ]; - $viewFile = __DIR__ . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'redirect.php'; - $response = Yii::$app->getResponse(); $response->content = Yii::$app->getView()->renderFile($viewFile, $viewData); return $response; @@ -192,10 +256,11 @@ class AuthAction extends Action } /** - * @param OpenId $client provider instance. - * @return \yii\web\Response action response. - * @throws Exception on failure - * @throws \yii\web\HttpException + * Performs OpenID auth flow. + * @param OpenId $client auth client instance. + * @return Response action response. + * @throws Exception on failure. + * @throws HttpException on failure. */ protected function authOpenId($client) { @@ -223,10 +288,11 @@ class AuthAction extends Action } /** - * @param OAuth1 $provider - * @return \yii\web\Response + * Performs OAuth1 auth flow. + * @param OAuth1 $client auth client instance. + * @return Response action response. */ - protected function authOAuth1($provider) + protected function authOAuth1($client) { // user denied error if (isset($_GET['denied'])) { @@ -239,24 +305,25 @@ class AuthAction extends Action if (!isset($oauthToken)) { // Get request token. - $requestToken = $provider->fetchRequestToken(); + $requestToken = $client->fetchRequestToken(); // Get authorization URL. - $url = $provider->buildAuthUrl($requestToken); + $url = $client->buildAuthUrl($requestToken); // Redirect to authorization URL. return Yii::$app->getResponse()->redirect($url); } else { // Upgrade to access token. - $accessToken = $provider->fetchAccessToken(); - return $this->authSuccess($provider); + $accessToken = $client->fetchAccessToken(); + return $this->authSuccess($client); } } /** - * @param OAuth2 $provider - * @return \yii\web\Response - * @throws \yii\base\Exception + * Performs OAuth2 auth flow. + * @param OAuth2 $client auth client instance. + * @return Response action response. + * @throws \yii\base\Exception on failure. */ - protected function authOAuth2($provider) + protected function authOAuth2($client) { if (isset($_GET['error'])) { if ($_GET['error'] == 'access_denied') { @@ -278,14 +345,14 @@ class AuthAction extends Action // Get the access_token and save them to the session. if (isset($_GET['code'])) { $code = $_GET['code']; - $token = $provider->fetchAccessToken($code); + $token = $client->fetchAccessToken($code); if (!empty($token)) { - return $this->authSuccess($provider); + return $this->authSuccess($client); } else { return $this->redirectCancel(); } } else { - $url = $provider->buildAuthUrl(); + $url = $client->buildAuthUrl(); return Yii::$app->getResponse()->redirect($url); } } diff --git a/extensions/yii/authclient/Collection.php b/extensions/yii/authclient/Collection.php index d94e619..2868d1c 100644 --- a/extensions/yii/authclient/Collection.php +++ b/extensions/yii/authclient/Collection.php @@ -18,7 +18,7 @@ use Yii; * * ~~~ * 'components' => [ - * 'auth' => [ + * 'authClientCollection' => [ * 'class' => 'yii\authclient\Collection', * 'clients' => [ * 'google' => [ diff --git a/extensions/yii/authclient/widgets/Choice.php b/extensions/yii/authclient/widgets/Choice.php index 667e2a0..fe20735 100644 --- a/extensions/yii/authclient/widgets/Choice.php +++ b/extensions/yii/authclient/widgets/Choice.php @@ -29,7 +29,9 @@ use yii\authclient\ClientInterface; * For example: * * ~~~ - * + * ['site/auth'] + * ]); ?> *
    * getClients() as $client): ?> *
  • clientLink($client); ?>
  • @@ -38,6 +40,8 @@ use yii\authclient\ClientInterface; * * ~~~ * + * @see \yii\authclient\AuthAction + * * @property ClientInterface[] $providers auth providers list. * @property array $baseAuthUrl configuration for the external services base authentication URL. * @@ -54,7 +58,7 @@ class Choice extends Widget * @var string name of the auth client collection application component. * This component will be used to fetch {@link services} value if it is not set. */ - public $clientCollection = 'auth'; + public $clientCollection = 'authClientCollection'; /** * @var array configuration for the external clients base authentication URL. */ From c1a810ba8d50d939f5a3e726185b1b7df8ba5a7f Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Wed, 25 Dec 2013 17:07:16 +0200 Subject: [PATCH 37/37] Documentation at "yii\authclient" updated. --- extensions/yii/authclient/README.md | 55 +++++++++++++++++++++- extensions/yii/authclient/clients/Facebook.php | 34 +++++++++++++ extensions/yii/authclient/clients/GitHub.php | 18 +++++++ extensions/yii/authclient/clients/GoogleOAuth.php | 18 +++++++ extensions/yii/authclient/clients/GoogleOpenId.php | 16 +++++++ extensions/yii/authclient/clients/LinkedIn.php | 34 +++++++++++++ extensions/yii/authclient/clients/Twitter.php | 34 +++++++++++++ extensions/yii/authclient/clients/YandexOAuth.php | 18 +++++++ extensions/yii/authclient/clients/YandexOpenId.php | 16 +++++++ .../yii/authclient/widgets/assets/authchoice.js | 11 +++++ 10 files changed, 253 insertions(+), 1 deletion(-) diff --git a/extensions/yii/authclient/README.md b/extensions/yii/authclient/README.md index b4c57ae..5aff122 100644 --- a/extensions/yii/authclient/README.md +++ b/extensions/yii/authclient/README.md @@ -27,4 +27,57 @@ to the require section of your composer.json. Usage & Documentation --------------------- -This extension... \ No newline at end of file +This extension provides the ability of the authentication via external credentials providers. +It covers OpenID, OAuth1 and OAuth2 protocols. + +You need to setup auth client collection application component: + +``` +'components' => [ + 'authClientCollection' => [ + 'class' => 'yii\authclient\Collection', + 'clients' => [ + 'google' => [ + 'class' => 'yii\authclient\clients\GoogleOpenId' + ], + 'facebook' => [ + 'class' => 'yii\authclient\clients\Facebook', + 'clientId' => 'facebook_client_id', + 'clientSecret' => 'facebook_client_secret', + ], + ], + ] + ... +] +``` + +Then you need to apply [[yii\authclient\AuthAction]] to some of your web controllers: + +``` +class SiteController extends Controller +{ + public function actions() + { + return [ + 'auth' => [ + 'class' => 'yii\authclient\AuthAction', + 'successCallback' => [$this, 'successCallback'], + ], + ] + } + + public function successCallback($client) + { + $atributes = $client->getUserAttributes(); + // user login or signup comes here + } +} +``` + +You may use [[yii\authclient\widgets\Choice]] to compose auth client selection: + +``` + ['site/auth'] +]); ?> +``` \ No newline at end of file diff --git a/extensions/yii/authclient/clients/Facebook.php b/extensions/yii/authclient/clients/Facebook.php index 7c17c27..4a5014d 100644 --- a/extensions/yii/authclient/clients/Facebook.php +++ b/extensions/yii/authclient/clients/Facebook.php @@ -13,6 +13,24 @@ use yii\authclient\OAuth2; * Facebook allows authentication via Facebook OAuth. * In order to use Facebook OAuth you must register your application at [[https://developers.facebook.com/apps]]. * + * Example application configuration: + * + * ~~~ + * 'components' => [ + * 'authClientCollection' => [ + * 'class' => 'yii\authclient\Collection', + * 'clients' => [ + * 'facebook' => [ + * 'class' => 'yii\authclient\clients\Facebook', + * 'clientId' => 'facebook_client_id', + * 'clientSecret' => 'facebook_client_secret', + * ], + * ], + * ] + * ... + * ] + * ~~~ + * * @see https://developers.facebook.com/apps * @see http://developers.facebook.com/docs/reference/api * @@ -45,4 +63,20 @@ class Facebook extends OAuth2 { return $this->api('me', 'GET'); } + + /** + * @inheritdoc + */ + protected function defaultName() + { + return 'facebook'; + } + + /** + * @inheritdoc + */ + protected function defaultTitle() + { + return 'Facebook'; + } } \ No newline at end of file diff --git a/extensions/yii/authclient/clients/GitHub.php b/extensions/yii/authclient/clients/GitHub.php index 796b86e..57f430d 100644 --- a/extensions/yii/authclient/clients/GitHub.php +++ b/extensions/yii/authclient/clients/GitHub.php @@ -13,6 +13,24 @@ use yii\authclient\OAuth2; * GitHub allows authentication via GitHub OAuth. * In order to use GitHub OAuth you must register your application at [[https://github.com/settings/applications/new]]. * + * Example application configuration: + * + * ~~~ + * 'components' => [ + * 'authClientCollection' => [ + * 'class' => 'yii\authclient\Collection', + * 'clients' => [ + * 'github' => [ + * 'class' => 'yii\authclient\clients\GitHub', + * 'clientId' => 'github_client_id', + * 'clientSecret' => 'github_client_secret', + * ], + * ], + * ] + * ... + * ] + * ~~~ + * * @see http://developer.github.com/v3/oauth/ * @see https://github.com/settings/applications/new * diff --git a/extensions/yii/authclient/clients/GoogleOAuth.php b/extensions/yii/authclient/clients/GoogleOAuth.php index 2fcd0c1..313358e 100644 --- a/extensions/yii/authclient/clients/GoogleOAuth.php +++ b/extensions/yii/authclient/clients/GoogleOAuth.php @@ -13,6 +13,24 @@ use yii\authclient\OAuth2; * GoogleOAuth allows authentication via Google OAuth. * In order to use Google OAuth you must register your application at [[https://code.google.com/apis/console#access]]. * + * Example application configuration: + * + * ~~~ + * 'components' => [ + * 'authClientCollection' => [ + * 'class' => 'yii\authclient\Collection', + * 'clients' => [ + * 'google' => [ + * 'class' => 'yii\authclient\clients\GoogleOAuth', + * 'clientId' => 'google_client_id', + * 'clientSecret' => 'google_client_secret', + * ], + * ], + * ] + * ... + * ] + * ~~~ + * * @see https://code.google.com/apis/console#access * @see https://developers.google.com/google-apps/contacts/v3/ * diff --git a/extensions/yii/authclient/clients/GoogleOpenId.php b/extensions/yii/authclient/clients/GoogleOpenId.php index cf5a3b4..8b463d6 100644 --- a/extensions/yii/authclient/clients/GoogleOpenId.php +++ b/extensions/yii/authclient/clients/GoogleOpenId.php @@ -13,6 +13,22 @@ use yii\authclient\OpenId; * GoogleOpenId allows authentication via Google OpenId. * Unlike Google OAuth you do not need to register your application anywhere in order to use Google OpenId. * + * Example application configuration: + * + * ~~~ + * 'components' => [ + * 'authClientCollection' => [ + * 'class' => 'yii\authclient\Collection', + * 'clients' => [ + * 'google' => [ + * 'class' => 'yii\authclient\clients\GoogleOpenId' + * ], + * ], + * ] + * ... + * ] + * ~~~ + * * @author Paul Klimov * @since 2.0 */ diff --git a/extensions/yii/authclient/clients/LinkedIn.php b/extensions/yii/authclient/clients/LinkedIn.php index c6de848..130fda3 100644 --- a/extensions/yii/authclient/clients/LinkedIn.php +++ b/extensions/yii/authclient/clients/LinkedIn.php @@ -15,6 +15,24 @@ use Yii; * LinkedIn allows authentication via LinkedIn OAuth. * In order to use linkedIn OAuth you must register your application at [[https://www.linkedin.com/secure/developer]]. * + * Example application configuration: + * + * ~~~ + * 'components' => [ + * 'authClientCollection' => [ + * 'class' => 'yii\authclient\Collection', + * 'clients' => [ + * 'linkedin' => [ + * 'class' => 'yii\authclient\clients\LinkedIn', + * 'clientId' => 'linkedin_client_id', + * 'clientSecret' => 'linkedin_client_secret', + * ], + * ], + * ] + * ... + * ] + * ~~~ + * * @see http://developer.linkedin.com/documents/authentication * @see https://www.linkedin.com/secure/developer * @see http://developer.linkedin.com/apis @@ -130,4 +148,20 @@ class LinkedIn extends OAuth2 protected function generateAuthState() { return sha1(uniqid(get_class($this), true)); } + + /** + * @inheritdoc + */ + protected function defaultName() + { + return 'linkedin'; + } + + /** + * @inheritdoc + */ + protected function defaultTitle() + { + return 'LinkedIn'; + } } \ No newline at end of file diff --git a/extensions/yii/authclient/clients/Twitter.php b/extensions/yii/authclient/clients/Twitter.php index 839cfce..e3c99f9 100644 --- a/extensions/yii/authclient/clients/Twitter.php +++ b/extensions/yii/authclient/clients/Twitter.php @@ -13,6 +13,24 @@ use yii\authclient\OAuth1; * Twitter allows authentication via Twitter OAuth. * In order to use Twitter OAuth you must register your application at [[https://dev.twitter.com/apps/new]]. * + * Example application configuration: + * + * ~~~ + * 'components' => [ + * 'authClientCollection' => [ + * 'class' => 'yii\authclient\Collection', + * 'clients' => [ + * 'twitter' => [ + * 'class' => 'yii\authclient\clients\Twitter', + * 'consumerKey' => 'twitter_consumer_key', + * 'consumerSecret' => 'twitter_consumer_secret', + * ], + * ], + * ] + * ... + * ] + * ~~~ + * * @see https://dev.twitter.com/apps/new * @see https://dev.twitter.com/docs/api * @@ -53,4 +71,20 @@ class Twitter extends OAuth1 { return $this->api('account/verify_credentials.json', 'GET'); } + + /** + * @inheritdoc + */ + protected function defaultName() + { + return 'twitter'; + } + + /** + * @inheritdoc + */ + protected function defaultTitle() + { + return 'Twitter'; + } } \ No newline at end of file diff --git a/extensions/yii/authclient/clients/YandexOAuth.php b/extensions/yii/authclient/clients/YandexOAuth.php index 83e1072..667d019 100644 --- a/extensions/yii/authclient/clients/YandexOAuth.php +++ b/extensions/yii/authclient/clients/YandexOAuth.php @@ -13,6 +13,24 @@ use yii\authclient\OAuth2; * YandexOAuth allows authentication via Yandex OAuth. * In order to use Yandex OAuth you must register your application at [[https://oauth.yandex.ru/client/new]]. * + * Example application configuration: + * + * ~~~ + * 'components' => [ + * 'authClientCollection' => [ + * 'class' => 'yii\authclient\Collection', + * 'clients' => [ + * 'yandex' => [ + * 'class' => 'yii\authclient\clients\YandexOAuth', + * 'clientId' => 'yandex_client_id', + * 'clientSecret' => 'yandex_client_secret', + * ], + * ], + * ] + * ... + * ] + * ~~~ + * * @see https://oauth.yandex.ru/client/new * @see http://api.yandex.ru/login/doc/dg/reference/response.xml * diff --git a/extensions/yii/authclient/clients/YandexOpenId.php b/extensions/yii/authclient/clients/YandexOpenId.php index 9aedf2d..20c3b81 100644 --- a/extensions/yii/authclient/clients/YandexOpenId.php +++ b/extensions/yii/authclient/clients/YandexOpenId.php @@ -13,6 +13,22 @@ use yii\authclient\OpenId; * YandexOpenId allows authentication via Yandex OpenId. * Unlike Yandex OAuth you do not need to register your application anywhere in order to use Yandex OpenId. * + * Example application configuration: + * + * ~~~ + * 'components' => [ + * 'authClientCollection' => [ + * 'class' => 'yii\authclient\Collection', + * 'clients' => [ + * 'yandex' => [ + * 'class' => 'yii\authclient\clients\YandexOpenId' + * ], + * ], + * ] + * ... + * ] + * ~~~ + * * @author Paul Klimov * @since 2.0 */ diff --git a/extensions/yii/authclient/widgets/assets/authchoice.js b/extensions/yii/authclient/widgets/assets/authchoice.js index ed59e9a..3e019d9 100644 --- a/extensions/yii/authclient/widgets/assets/authchoice.js +++ b/extensions/yii/authclient/widgets/assets/authchoice.js @@ -1,3 +1,14 @@ +/** + * Yii auth choice widget. + * + * This is the JavaScript widget used by the yii\authclient\widgets\Choice widget. + * + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @author Paul Klimov + * @since 2.0 + */ jQuery(function($) { $.fn.authchoice = function(options) { options = $.extend({