From b226d1878f729bad02cb8b0bba35629b06455521 Mon Sep 17 00:00:00 2001 From: Rachman Chavik Date: Mon, 19 Aug 2013 08:44:38 +0700 Subject: [PATCH 1/8] Migrations: enlarge columns for encrypted values --- .../Migration/1376749679_enlarge_secrets.php | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 Config/Migration/1376749679_enlarge_secrets.php diff --git a/Config/Migration/1376749679_enlarge_secrets.php b/Config/Migration/1376749679_enlarge_secrets.php new file mode 100644 index 0000000..95b8ac2 --- /dev/null +++ b/Config/Migration/1376749679_enlarge_secrets.php @@ -0,0 +1,59 @@ + array( + 'alter_field' => array( + 'clients' => array( + 'client_secret' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 132, 'collate' => 'utf8_general_ci', 'charset' => 'utf8', 'after' => 'client_id'), + ), + ), + ), + + 'down' => array( + 'alter_field' => array( + 'clients' => array( + 'client_secret' => array('type' => 'string', 'null' => false, 'default' => NULL, 'length' => 40, 'collate' => 'utf8_general_ci', 'charset' => 'utf8', 'after' => 'client_id'), + ), + ), + ), + ); + +/** + * Before migration callback + * + * @param string $direction, up or down direction of migration process + * @return boolean Should process continue + * @access public + */ + public function before($direction) { + return true; + } + +/** + * After migration callback + * + * @param string $direction, up or down direction of migration process + * @return boolean Should process continue + * @access public + */ + public function after($direction) { + return true; + } + +} From b89c0b33c9cc6cc535bb41563c166c0d98a58353 Mon Sep 17 00:00:00 2001 From: Rachman Chavik Date: Mon, 19 Aug 2013 09:36:34 +0700 Subject: [PATCH 2/8] Migrations: additional fields to clients table --- ...376879144_clients_add_name_date_fields.php | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 Config/Migration/1376879144_clients_add_name_date_fields.php diff --git a/Config/Migration/1376879144_clients_add_name_date_fields.php b/Config/Migration/1376879144_clients_add_name_date_fields.php new file mode 100644 index 0000000..ed12008 --- /dev/null +++ b/Config/Migration/1376879144_clients_add_name_date_fields.php @@ -0,0 +1,80 @@ + array( + 'create_field' => array( + 'clients' => array( + 'name' => array( + 'type' => 'string', + 'null' => false, + 'default' => null, + 'length' => 256, + 'collate' => + 'utf8_general_ci', + 'charset' => 'utf8', + 'after' => 'client_id', + ), + 'created' => array( + 'type' => 'datetime', + 'after' => 'user_id', + 'null' => true, + ), + 'modified' => array( + 'type' => 'datetime', + 'after' => 'created', + 'null' => true, + ), + ), + ), + ), + + 'down' => array( + 'drop_field' => array( + 'clients' => array( + 'name', + 'created', + 'modified', + ), + ), + ), + ); + +/** + * Before migration callback + * + * @param string $direction, up or down direction of migration process + * @return boolean Should process continue + * @access public + */ + public function before($direction) { + return true; + } + +/** + * After migration callback + * + * @param string $direction, up or down direction of migration process + * @return boolean Should process continue + * @access public + */ + public function after($direction) { + return true; + } + +} From c35420358d5b0bc10be14b6c8fc1d054fb088f7c Mon Sep 17 00:00:00 2001 From: Rachman Chavik Date: Sat, 17 Aug 2013 20:39:53 +0700 Subject: [PATCH 3/8] Adding OAuthUtility class Currently, this class implements the shortcut for [en|de]cryption methods. In the future, this class would be used for the oauth-php interface implementations. --- Lib/OAuthUtility.php | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 Lib/OAuthUtility.php diff --git a/Lib/OAuthUtility.php b/Lib/OAuthUtility.php new file mode 100644 index 0000000..5b93a62 --- /dev/null +++ b/Lib/OAuthUtility.php @@ -0,0 +1,41 @@ + Date: Sat, 17 Aug 2013 20:52:26 +0700 Subject: [PATCH 4/8] Encrypt secrets instead of hashing --- Controller/Component/OAuthComponent.php | 12 +++++++----- Model/Behavior/HashedFieldBehavior.php | 6 +++--- Model/Client.php | 19 +++++++++++-------- README.markdown | 7 ++++++- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/Controller/Component/OAuthComponent.php b/Controller/Component/OAuthComponent.php index df1fd6e..a7ec7bf 100644 --- a/Controller/Component/OAuthComponent.php +++ b/Controller/Component/OAuthComponent.php @@ -399,15 +399,17 @@ public function __call($name, $arguments) { */ public function checkClientCredentials($client_id, $client_secret = null) { $conditions = array('client_id' => $client_id); - if ($client_secret) { - $conditions['client_secret'] = $client_secret; - } $client = $this->Client->find('first', array( 'conditions' => $conditions, 'recursive' => -1 )); - if ($client) { - return $client['Client']; + if ($client && $client_secret) { + $decrypted = self::decrypt($client['Client']['client_secret']); + if ($decrypted == $client_secret) { + return $client['Client']; + } else { + return false; + } }; return false; } diff --git a/Model/Behavior/HashedFieldBehavior.php b/Model/Behavior/HashedFieldBehavior.php index 8401761..a6dc16e 100644 --- a/Model/Behavior/HashedFieldBehavior.php +++ b/Model/Behavior/HashedFieldBehavior.php @@ -1,7 +1,7 @@ data[$Model->alias])) { $data = $Model->data[$Model->alias][$field]; - $Model->data[$Model->alias][$field] = Security::hash($data, null, true); + $Model->data[$Model->alias][$field] = OAuthUtility::hash($data); } } return true; @@ -52,7 +52,7 @@ public function beforeFind(Model $Model, $queryData) { } if (isset($queryField)) { $data = $conditions[$queryField]; - $conditions[$queryField] = Security::hash($data, null, true); + $conditions[$queryField] = OAuthUtility::hash($data); } } return $queryData; diff --git a/Model/Client.php b/Model/Client.php index 1331cbb..5b56f88 100644 --- a/Model/Client.php +++ b/Model/Client.php @@ -1,6 +1,7 @@ array( - 'fields' => array( - 'client_secret' - ), - ), - ); - /** * hasMany associations * @@ -164,6 +157,16 @@ public function newClientSecret() { return OAuthComponent::hash($str); } +/** + * Encrypt client secret + */ + public function beforeSave($options) { + if (isset($this->data[$this->alias]['client_secret'])) { + $this->data[$this->alias]['client_secret'] = OAuthUtility::encrypt($this->data[$this->alias]['client_secret']); + } + return true; + } + public function afterSave($created) { if ($this->addClientSecret) { $this->data['Client']['client_secret'] = $this->addClientSecret; diff --git a/README.markdown b/README.markdown index 806a7bf..c50e947 100644 --- a/README.markdown +++ b/README.markdown @@ -121,7 +121,12 @@ Array( The method includes various schemes for generating client id's, [pick your favourite](https://github.com/thomseddon/cakephp-oauth-server/blob/master/Model/Client.php#L122). -**NOTE:** This convenience method will generate a random client secret __and hash it__ for security before storage. Although it will pass back the actual raw client secret when you first add a new client, it is not possible to ever determine this from the hash stored in the database. So if the client forgets their secret, [a new one will have to be issued](https://github.com/thomseddon/cakephp-oauth-server/blob/master/Model/Client.php#L139). +**NOTE:** This convenience method will generate a random client secret +__and encrypt it__ for security before storage using `Security::rijndael()` +method. The default behavior is to use `Security.salt` as encryption key. +Thus, it's important to treat it as __extremely sensitive data__. + +Use `OAuthUtility::decrypt()` to obtain the client_secret. ### Included Endpoints From b2b59114839267916177048236e27ae2396fddb9 Mon Sep 17 00:00:00 2001 From: Rachman Chavik Date: Wed, 21 Aug 2013 07:49:52 +0700 Subject: [PATCH 5/8] Deprecate OAuthComponent::hash() --- Controller/Component/OAuthComponent.php | 1 + Model/Client.php | 2 +- Model/OAuthAppModel.php | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Controller/Component/OAuthComponent.php b/Controller/Component/OAuthComponent.php index a7ec7bf..c15e472 100644 --- a/Controller/Component/OAuthComponent.php +++ b/Controller/Component/OAuthComponent.php @@ -326,6 +326,7 @@ public function user($field = null, $token = null) { * * @param string $password * @return string Hashed password + * @deprecated Will be removed in future version */ public static function hash($password) { return Security::hash($password, null, true); diff --git a/Model/Client.php b/Model/Client.php index 5b56f88..9a0e639 100644 --- a/Model/Client.php +++ b/Model/Client.php @@ -154,7 +154,7 @@ public function newClientSecret() { while ($length--) { $str .= $chars[mt_rand(0, $count - 1)]; } - return OAuthComponent::hash($str); + return OAuthUtility::hash($str); } /** diff --git a/Model/OAuthAppModel.php b/Model/OAuthAppModel.php index 48201f4..a971dd8 100644 --- a/Model/OAuthAppModel.php +++ b/Model/OAuthAppModel.php @@ -1,5 +1,7 @@ Date: Mon, 19 Aug 2013 10:49:13 +0700 Subject: [PATCH 6/8] ClientsShell for creating/listing client records Easier to test/list clients records --- Console/Command/ClientsShell.php | 130 +++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 Console/Command/ClientsShell.php diff --git a/Console/Command/ClientsShell.php b/Console/Command/ClientsShell.php new file mode 100644 index 0000000..09a4918 --- /dev/null +++ b/Console/Command/ClientsShell.php @@ -0,0 +1,130 @@ +description('Client Utility') + ->addSubCommand('list', array( + 'help' => 'List existing client records', + 'parser' => array( + 'options' => array( + 'secret' => array( + 'help' => 'Display secrets', + 'short' => 's', + 'boolean' => true, + ), + ), + ), + )) + ->addSubCommand('add', array( + 'help' => 'Add a new client', + 'parser' => array( + 'options' => array( + 'name' => array( + 'required' => true, + 'help' => 'Client Name', + 'short' => 'n', + ), + 'redirect_uri' => array( + 'required' => true, + 'help' => 'Redirect URI', + 'short' => 'u', + ), + ), + ), + )); + } + +/** + * Shell entry point + */ + public function main() { + $method = null; + if (isset($this->args[0])) { + $method = $this->args[0]; + } + + switch ($method) { + case 'list': + $this->_clients(); + break; + + default: + $this->_displayHelp(); + break; + } + } + +/** + * List all client records + */ + protected function _clients() { + $clients = $this->Client->find('all', array( + 'recursive' => -1, + )); + $this->out(""); + foreach ($clients as $data) { + $client = $data['Client']; + $this->out(sprintf('%-15s: %s', 'Client Id', $client['client_id'])); + $this->out(sprintf('%-15s: %s', 'Client Name', $client['name'])); + if ($this->params['secret']) { + $secret = OAuthUtility::decrypt($client['client_secret']); + $this->out(sprintf('%-15s: %s', 'Client Secret', $secret)); + } + $this->out(sprintf('%-15s: %s', 'Redirect URI', $client['redirect_uri'])); + $this->out(""); + } + $this->out(sprintf('%d record(s) found', count($clients))); + } + +/** + * Add a new client record + */ + public function add() { + if (empty($this->params['name'])) { + return $this->error('Please provide `name`'); + } + if (empty($this->params['redirect_uri'])) { + return $this->error('Please provide `redirect_uri`'); + } + if (!Validation::url($this->params['redirect_uri'])) { + return $this->error('Please provide a valid `redirect_uri`'); + } + $client = $this->Client->create(array( + 'name' => $this->params['name'], + 'redirect_uri' => $this->params['redirect_uri'], + )); + $client = $this->Client->add($client); + if (!$client) { + $this->err('Unable to add client record'); + if (isset($this->Client->validationErrors)) { + $this->error('Validation error', print_r($this->Client->validationErrors, true)); + } + return; + } + $this->out("Client successfully added:\n"); + $this->out(sprintf("\tClient id: %s", $client['Client']['client_id'])); + $this->out(sprintf("\tClient name: %s", $client['Client']['name'])); + $this->out(sprintf("\tClient secret: %s", $this->Client->addClientSecret)); + $this->out(); + } + +} From f36a6c566b714f9602f85bfdf2afc3f526d953bb Mon Sep 17 00:00:00 2001 From: Rachman Chavik Date: Sat, 17 Aug 2013 20:18:14 +0700 Subject: [PATCH 7/8] Refactor interface implementations into OAuthUtility --- Controller/Component/OAuthComponent.php | 314 +----------------- Lib/OAuthUtility.php | 414 +++++++++++++++++++++++- 2 files changed, 425 insertions(+), 303 deletions(-) diff --git a/Controller/Component/OAuthComponent.php b/Controller/Component/OAuthComponent.php index c15e472..86ffc01 100644 --- a/Controller/Component/OAuthComponent.php +++ b/Controller/Component/OAuthComponent.php @@ -21,21 +21,9 @@ App::uses('Security', 'Utility'); App::uses('Hash', 'Utility'); App::uses('AuthComponent', 'Controller'); +App::uses('OAuthUtility', 'OAuth.Lib'); -App::import('Vendor', 'oauth2-php/lib/OAuth2'); -App::import('Vendor', 'oauth2-php/lib/IOAuth2Storage'); -App::import('Vendor', 'oauth2-php/lib/IOAuth2RefreshTokens'); -App::import('Vendor', 'oauth2-php/lib/IOAuth2GrantUser'); -App::import('Vendor', 'oauth2-php/lib/IOAuth2GrantCode'); - -class OAuthComponent extends Component implements IOAuth2Storage, IOAuth2RefreshTokens, IOAuth2GrantUser, IOAuth2GrantCode { - -/** - * AccessToken object. - * - * @var object - */ - public $AccessToken; +class OAuthComponent extends Component { /** * Array of allowed actions @@ -82,51 +70,6 @@ class OAuthComponent extends Component implements IOAuth2Storage, IOAuth2Refresh 'fields' => array('username' => 'username', 'password' => 'password') ); -/** - * AuthCode object. - * - * @var object - */ - public $AuthCode; - -/** - * Clients object. - * - * @var object - */ - public $Client; - -/** - * Array of globally supported grant types - * - * By default = array('authorization_code', 'refresh_token', 'password'); - * Other grant mechanisms are not supported in the current release - * - * @var array - */ - public $grantTypes = array('authorization_code', 'refresh_token', 'password'); - -/** - * OAuth2 Object - * - * @var object - */ - public $OAuth2; - -/** - * RefreshToken object. - * - * @var object - */ - public $RefreshToken; - -/** - * User object - * - * @var object - */ - public $User; - /** * Static storage for current user * @@ -141,11 +84,7 @@ class OAuthComponent extends Component implements IOAuth2Storage, IOAuth2Refresh */ public function __construct(ComponentCollection $collection, $settings = array()) { parent::__construct($collection, $settings); - $this->OAuth2 = new OAuth2($this); - $this->AccessToken = ClassRegistry::init(array('class' => 'OAuth.AccessToken', 'alias' => 'AccessToken')); - $this->AuthCode = ClassRegistry::init(array('class' => 'OAuth.AuthCode', 'alias' => 'AuthCode')); - $this->Client = ClassRegistry::init(array('class' => 'OAuth.Client', 'alias' => 'Client')); - $this->RefreshToken = ClassRegistry::init(array('class' => 'OAuth.RefreshToken', 'alias' => 'RefreshToken')); + $this->OAuthUtility = new OAuthUtility(); } /** @@ -201,7 +140,7 @@ public function startup(Controller $controller) { try { $this->isAuthorized(); - $this->user(null, $this->AccessToken->id); + $this->user(null, $this->OAuthUtility->AccessToken->id); } catch (OAuth2AuthenticateException $e) { $e->sendHttpResponse(); return false; @@ -219,8 +158,8 @@ public function startup(Controller $controller) { */ public function isAuthorized() { try { - $this->AccessToken->id = $this->getBearerToken(); - $this->verifyAccessToken($this->AccessToken->id); + $this->OAuthUtility->AccessToken->id = $this->OAuthUtility->getBearerToken(); + $this->OAuthUtility->verifyAccessToken($this->OAuthUtility->AccessToken->id); } catch (OAuth2AuthenticateException $e) { return false; } @@ -295,7 +234,7 @@ public function deny($action = null) { */ public function user($field = null, $token = null) { if (!$this->_user) { - $this->AccessToken->bindModel(array( + $this->OAuthUtility->AccessToken->bindModel(array( 'belongsTo' => array( 'User' => array( 'className' => $this->authenticate['userModel'], @@ -303,7 +242,7 @@ public function user($field = null, $token = null) { ) ) )); - $token = empty($token) ? $this->getBearerToken() : $token; + $token = empty($token) ? $this->OAuthUtility->getBearerToken() : $token; $data = $this->AccessToken->find('first', array( 'conditions' => array('oauth_token' => $token), 'recursive' => 1 @@ -354,9 +293,9 @@ public function invalidateUserTokens($user_id, $tokens = 'both') { * @return mixed */ public function __get($name) { - if (isset($this->OAuth2->{$name})) { + if (isset($this->OAuthUtility->OAuth2->{$name})) { try { - return $this->OAuth2->{$name}; + return $this->OAuthUtility->OAuth2->{$name}; } catch (Exception $e) { $e->sendHttpResponse(); } @@ -372,9 +311,9 @@ public function __get($name) { * @throws Exception */ public function __call($name, $arguments) { - if (method_exists($this->OAuth2, $name)) { + if (method_exists($this->OAuthUtility->OAuth2, $name)) { try { - return call_user_func_array(array($this->OAuth2, $name), $arguments); + return call_user_func_array(array($this->OAuthUtility->OAuth2, $name), $arguments); } catch (Exception $e) { if (method_exists($e, 'sendHttpResponse')) { $e->sendHttpResponse(); @@ -384,233 +323,4 @@ public function __call($name, $arguments) { } } -/** - * Below are the library interface implementations - * - */ - -/** - * Check client details are valid - * - * @see IOAuth2Storage::checkClientCredentials(). - * - * @param string $client_id - * @param string $client_secret - * @return mixed array of client credentials if valid, false if not - */ - public function checkClientCredentials($client_id, $client_secret = null) { - $conditions = array('client_id' => $client_id); - $client = $this->Client->find('first', array( - 'conditions' => $conditions, - 'recursive' => -1 - )); - if ($client && $client_secret) { - $decrypted = self::decrypt($client['Client']['client_secret']); - if ($decrypted == $client_secret) { - return $client['Client']; - } else { - return false; - } - }; - return false; - } - -/** - * Get client details - * - * @see IOAuth2Storage::getClientDetails(). - * - * @param string $client_id - * @return boolean - */ - public function getClientDetails($client_id) { - $client = $this->Client->find('first', array( - 'conditions' => array('client_id' => $client_id), - 'fields' => array('client_id', 'redirect_uri'), - 'recursive' => -1 - )); - if ($client) { - return $client['Client']; - } - return false; - } - -/** - * Retrieve access token - * - * @see IOAuth2Storage::getAccessToken(). - * - * @param string $oauth_token - * @return mixed AccessToken array if valid, null if not - */ - public function getAccessToken($oauth_token) { - $accessToken = $this->AccessToken->find('first', array( - 'conditions' => array('oauth_token' => $oauth_token), - 'recursive' => -1, - )); - if ($accessToken) { - return $accessToken['AccessToken']; - } - return null; - } - -/** - * Set access token - * - * @see IOAuth2Storage::setAccessToken(). - * - * @param string $oauth_token - * @param string $client_id - * @param int $user_id - * @param string $expires - * @param string $scope - * @return boolean true if successfull, false if failed - */ - public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null) { - $data = array( - 'oauth_token' => $oauth_token, - 'client_id' => $client_id, - 'user_id' => $user_id, - 'expires' => $expires, - 'scope' => $scope - ); - $this->AccessToken->create(); - return $this->AccessToken->save(array('AccessToken' => $data)); - } - -/** - * Partial implementation, just checks globally avaliable grant types - * - * @see IOAuth2Storage::checkRestrictedGrantType() - * - * @param string $client_id - * @param string $grant_type - * @return boolean If grant type is availiable to client - */ - public function checkRestrictedGrantType($client_id, $grant_type) { - return in_array($grant_type, $this->grantTypes); - } - -/** - * Grant type: refresh_token - * - * @see IOAuth2RefreshTokens::getRefreshToken() - * - * @param string $refresh_token - * @return mixed RefreshToken if valid, null if not - */ - public function getRefreshToken($refresh_token) { - $refreshToken = $this->RefreshToken->find('first', array( - 'conditions' => array('refresh_token' => $refresh_token), - 'recursive' => -1 - )); - if ($refreshToken) { - return $refreshToken['RefreshToken']; - } - return null; - } - -/** - * Grant type: refresh_token - * - * @see IOAuth2RefreshTokens::setRefreshToken() - * - * @param string $refresh_token - * @param int $client_id - * @param string $user_id - * @param string $expires - * @param string $scope - * @return boolean true if successfull, false if fail - */ - public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) { - $data = array( - 'refresh_token' => $refresh_token, - 'client_id' => $client_id, - 'user_id' => $user_id, - 'expires' => $expires, - 'scope' => $scope - ); - $this->RefreshToken->create(); - return $this->RefreshToken->save(array('RefreshToken' => $data)); - } - -/** - * Grant type: refresh_token - * - * @see IOAuth2RefreshTokens::unsetRefreshToken() - * - * @param string $refresh_token - * @return boolean true if successfull, false if not - */ - public function unsetRefreshToken($refresh_token) { - return $this->RefreshToken->delete($refresh_token); - } - -/** - * Grant type: user_credentials - * - * @see IOAuth2GrantUser::checkUserCredentials() - * - * @param type $client_id - * @param type $username - * @param type $password - */ - public function checkUserCredentials($client_id, $username, $password) { - $user = $this->User->find('first', array( - 'conditions' => array( - $this->authenticate['fields']['username'] => $username, - $this->authenticate['fields']['password'] => AuthComponent::password($password) - ), - 'recursive' => -1 - )); - if ($user) { - return array('user_id' => $user['User'][$this->User->primaryKey]); - } - return false; - } - -/** - * Grant type: authorization_code - * - * @see IOAuth2GrantCode::getAuthCode() - * - * @param string $code - * @return AuthCode if valid, null of not - */ - public function getAuthCode($code) { - $authCode = $this->AuthCode->find('first', array( - 'conditions' => array('code' => $code), - 'recursive' => -1 - )); - if ($authCode) { - return $authCode['AuthCode']; - } - return null; - } - -/** - * Grant type: authorization_code - * - * @see IOAuth2GrantCode::setAuthCode(). - * - * @param string $code - * @param string $client_id - * @param int $user_id - * @param string $redirect_uri - * @param string $expires - * @param string $scope - * @return boolean true if successfull, otherwise false - */ - public function setAuthCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null) { - $data = array( - 'code' => $code, - 'client_id' => $client_id, - 'user_id' => $user_id, - 'redirect_uri' => $redirect_uri, - 'expires' => $expires, - 'scope' => $scope - ); - $this->AuthCode->create(); - return $this->AuthCode->save(array('AuthCode' => $data)); - } } diff --git a/Lib/OAuthUtility.php b/Lib/OAuthUtility.php index 5b93a62..208f329 100644 --- a/Lib/OAuthUtility.php +++ b/Lib/OAuthUtility.php @@ -1,8 +1,175 @@ + * @see https://github.com/thomseddon/cakephp-oauth-server + * + */ App::uses('Security', 'Utility'); -class OAuthUtility { +App::uses('Hash', 'Utility'); + +App::import('Vendor', 'oauth2-php/lib/OAuth2'); +App::import('Vendor', 'oauth2-php/lib/IOAuth2Storage'); +App::import('Vendor', 'oauth2-php/lib/IOAuth2RefreshTokens'); +App::import('Vendor', 'oauth2-php/lib/IOAuth2GrantUser'); +App::import('Vendor', 'oauth2-php/lib/IOAuth2GrantCode'); + +class OAuthUtility extends Object implements + IOAuth2Storage, + IOAuth2RefreshTokens, + IOAuth2GrantUser, + IOAuth2GrantCode +{ + +/** + * AccessToken object. + * + * @var object + */ + public $AccessToken; + +/** + * An array containing the model and fields to authenticate users against + * + * Inherits theses defaults: + * + * $this->OAuth->authenticate = array( + * 'userModel' => 'User', + * 'fields' => array( + * 'username' => 'username', + * 'password' => 'password' + * ) + * ); + * + * Which can be overridden in your beforeFilter: + * + * $this->OAuth->authenticate = array( + * 'fields' => array( + * 'username' => 'email' + * ) + * ); + * + * + * $this->OAuth->authenticate + * + * @var array + */ + public $authenticate; + +/** + * Defaults for $authenticate + * + * @var array + */ + protected $_authDefaults = array( + 'userModel' => 'User', + 'fields' => array('username' => 'username', 'password' => 'password') + ); + +/** + * AuthCode object. + * + * @var object + */ + public $AuthCode; + +/** + * Clients object. + * + * @var object + */ + public $Client; + +/** + * Array of globally supported grant types + * + * By default = array('authorization_code', 'refresh_token', 'password'); + * Other grant mechanisms are not supported in the current release + * + * @var array + */ + public $grantTypes = array('authorization_code', 'refresh_token', 'password'); + +/** + * OAuth2 Object + * + * @var object + */ + public $OAuth2; + +/** + * RefreshToken object. + * + * @var object + */ + public $RefreshToken; + +/** + * User object + * + * @var object + */ + public $User; + +/** + * Static storage for current user + * + * @var array + */ + protected $_user = false; + +/** + * Constructor - Adds class associations + * + * @see OAuth2::__construct(). + */ + public function __construct() { + $this->OAuth2 = new OAuth2($this); + $this->AccessToken = ClassRegistry::init(array('class' => 'OAuth.AccessToken', 'alias' => 'AccessToken')); + $this->AuthCode = ClassRegistry::init(array('class' => 'OAuth.AuthCode', 'alias' => 'AuthCode')); + $this->Client = ClassRegistry::init(array('class' => 'OAuth.Client', 'alias' => 'Client')); + $this->RefreshToken = ClassRegistry::init(array('class' => 'OAuth.RefreshToken', 'alias' => 'RefreshToken')); + } + +/** + * Fakes the OAuth2.php vendor class extension for variables + * + * @param string $name + * @return mixed + */ + public function __get($name) { + if (isset($this->OAuth2->{$name})) { + try { + return $this->OAuth2->{$name}; + } catch (Exception $e) { + $e->sendHttpResponse(); + } + } + } + +/** + * Fakes the OAuth2.php vendor class extension for methods + * + * @param string $name + * @param mixed $arguments + * @return mixed + * @throws Exception + */ + public function __call($name, $arguments) { + if (method_exists($this->OAuth2, $name)) { + try { + return call_user_func_array(array($this->OAuth2, $name), $arguments); + } catch (Exception $e) { + if (method_exists($e, 'sendHttpResponse')) { + $e->sendHttpResponse(); + } + throw $e; + } + } + } /** * Convenience method to encrypt strings using Security::rijndael() @@ -38,4 +205,249 @@ public static function hash($text) { return Security::hash($text, null, true); } +/** + * Convenience function to invalidate all a users tokens, for example when they change their password + * + * @param int $user_id + * @param string $tokens 'both' (default) to remove both AccessTokens and RefreshTokens or remove just one type using 'access' or 'refresh' + */ + public function invalidateUserTokens($user_id, $tokens = 'both') { + if ($tokens == 'access' || $tokens == 'both') { + $this->AccessToken->deleteAll(array('user_id' => $user_id), false); + } + if ($tokens == 'refresh' || $tokens == 'both') { + $this->RefreshToken->deleteAll(array('user_id' => $user_id), false); + } + } + +/** + * Below are the library interface implementations + * + */ + +/** + * Check client details are valid + * + * @see IOAuth2Storage::checkClientCredentials(). + * + * @param string $client_id + * @param string $client_secret + * @return mixed array of client credentials if valid, false if not + */ + public function checkClientCredentials($client_id, $client_secret = null) { + $conditions = array('client_id' => $client_id); + $client = $this->Client->find('first', array( + 'conditions' => $conditions, + 'recursive' => -1 + )); + if ($client) { + $decrypted = self::decrypt($client['Client']['client_secret']); + if ($decrypted == $client_secret) { + return $client['Client']; + } else { + return false; + } + }; + return false; + } + +/** + * Get client details + * + * @see IOAuth2Storage::getClientDetails(). + * + * @param string $client_id + * @return boolean + */ + public function getClientDetails($client_id) { + $client = $this->Client->find('first', array( + 'conditions' => array('client_id' => $client_id), + 'fields' => array('client_id', 'redirect_uri'), + 'recursive' => -1 + )); + if ($client) { + return $client['Client']; + } + return false; + } + +/** + * Retrieve access token + * + * @see IOAuth2Storage::getAccessToken(). + * + * @param string $oauth_token + * @return mixed AccessToken array if valid, null if not + */ + public function getAccessToken($oauth_token) { + $accessToken = $this->AccessToken->find('first', array( + 'conditions' => array('oauth_token' => $oauth_token), + 'recursive' => -1, + )); + if ($accessToken) { + return $accessToken['AccessToken']; + } + return null; + } + +/** + * Set access token + * + * @see IOAuth2Storage::setAccessToken(). + * + * @param string $oauth_token + * @param string $client_id + * @param int $user_id + * @param string $expires + * @param string $scope + * @return boolean true if successfull, false if failed + */ + public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null) { + $data = array( + 'oauth_token' => $oauth_token, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope + ); + $this->AccessToken->create(); + return $this->AccessToken->save(array('AccessToken' => $data)); + } + +/** + * Partial implementation, just checks globally avaliable grant types + * + * @see IOAuth2Storage::checkRestrictedGrantType() + * + * @param string $client_id + * @param string $grant_type + * @return boolean If grant type is availiable to client + */ + public function checkRestrictedGrantType($client_id, $grant_type) { + return in_array($grant_type, $this->grantTypes); + } + +/** + * Grant type: refresh_token + * + * @see IOAuth2RefreshTokens::getRefreshToken() + * + * @param string $refresh_token + * @return mixed RefreshToken if valid, null if not + */ + public function getRefreshToken($refresh_token) { + $refreshToken = $this->RefreshToken->find('first', array( + 'conditions' => array('refresh_token' => $refresh_token), + 'recursive' => -1 + )); + if ($refreshToken) { + return $refreshToken['RefreshToken']; + } + return null; + } + +/** + * Grant type: refresh_token + * + * @see IOAuth2RefreshTokens::setRefreshToken() + * + * @param string $refresh_token + * @param int $client_id + * @param string $user_id + * @param string $expires + * @param string $scope + * @return boolean true if successfull, false if fail + */ + public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) { + $data = array( + 'refresh_token' => $refresh_token, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'expires' => $expires, + 'scope' => $scope + ); + $this->RefreshToken->create(); + return $this->RefreshToken->save(array('RefreshToken' => $data)); + } + +/** + * Grant type: refresh_token + * + * @see IOAuth2RefreshTokens::unsetRefreshToken() + * + * @param string $refresh_token + * @return boolean true if successfull, false if not + */ + public function unsetRefreshToken($refresh_token) { + return $this->RefreshToken->delete($refresh_token); + } + +/** + * Grant type: user_credentials + * + * @see IOAuth2GrantUser::checkUserCredentials() + * + * @param type $client_id + * @param type $username + * @param type $password + */ + public function checkUserCredentials($client_id, $username, $password) { + $user = $this->User->find('first', array( + 'conditions' => array( + $this->authenticate['fields']['username'] => $username, + $this->authenticate['fields']['password'] => AuthComponent::password($password) + ), + 'recursive' => -1 + )); + if ($user) { + return array('user_id' => $user['User'][$this->User->primaryKey]); + } + return false; + } + +/** + * Grant type: authorization_code + * + * @see IOAuth2GrantCode::getAuthCode() + * + * @param string $code + * @return AuthCode if valid, null of not + */ + public function getAuthCode($code) { + $authCode = $this->AuthCode->find('first', array( + 'conditions' => array('code' => $code), + 'recursive' => -1 + )); + if ($authCode) { + return $authCode['AuthCode']; + } + return null; + } + +/** + * Grant type: authorization_code + * + * @see IOAuth2GrantCode::setAuthCode(). + * + * @param string $code + * @param string $client_id + * @param int $user_id + * @param string $redirect_uri + * @param string $expires + * @param string $scope + * @return boolean true if successfull, otherwise false + */ + public function setAuthCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null) { + $data = array( + 'code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope + ); + $this->AuthCode->create(); + return $this->AuthCode->save(array('AuthCode' => $data)); + } + } From efb7a423626af02eff9808de917f9b421342f7ff Mon Sep 17 00:00:00 2001 From: Rachman Chavik Date: Sat, 3 Aug 2013 16:54:29 +0700 Subject: [PATCH 8/8] Adding OAuthAuthenticate class --- .../Component/Auth/OAuthAuthenticate.php | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 Controller/Component/Auth/OAuthAuthenticate.php diff --git a/Controller/Component/Auth/OAuthAuthenticate.php b/Controller/Component/Auth/OAuthAuthenticate.php new file mode 100644 index 0000000..203852b --- /dev/null +++ b/Controller/Component/Auth/OAuthAuthenticate.php @@ -0,0 +1,90 @@ +OAuthUtility = new OAuthUtility(); + } + +/** + * Checks wether request has credential data + * + * @param CakeRequest $request Request object + * @return bool True when request has token/bearer data + */ + protected function _hasCredentials(CakeRequest $request) { + return isset($request->query['access_token']) || $request->header('Authorization'); + } + +/** + * Authenticate a user based on the request information + * + * @see BaseAuthenticate + */ + public function authenticate(CakeRequest $request, CakeResponse $response) { + return $this->getUser($request); + } + +/** + * Gets a user based on information in the request. + * + * @param CakeRequest $request Request object + * @return mixed Either false or an array of user information + * @see OAuth2::getBearerToken() + */ + public function getUser($request) { + if (!$this->_hasCredentials($request)) { + return false; + } + $token = $this->OAuthUtility->getBearerToken(); + if (!$token) { + return false; + } + + $AccessToken = ClassRegistry::init('OAuth.AccessToken'); + $accessToken = $AccessToken->find('first', array( + 'conditions' => array( + 'oauth_token' => $token, + ), + )); + + if (empty($accessToken['AccessToken']['user_id'])) { + return false; + } + + $fields = $this->settings['fields']; + list($plugin, $model) = pluginSplit($this->settings['userModel']); + $User = ClassRegistry::init($this->settings['userModel']); + + $conditions = array( + $model . '.' . $User->primaryKey => $accessToken['AccessToken']['user_id'], + ); + + $result = $User->find('first', array( + 'conditions' => $conditions, + 'recursive' => (int)$this->settings['recursive'], + 'contain' => $this->settings['contain'], + )); + if (empty($result[$model])) { + return false; + } + $user = $result[$model]; + unset($user[$fields['password']]); + unset($result[$model]); + return array_merge($user, $result); + } + +}